SensorT Mini:超迷你多功能运动开发板

这个小板依然是一个个人项目,个人很喜欢精致小巧的东西,所以做了这个小板,实现了很多小功能,除了基本的传感器系列外,这个小板还可以实现运行识别和运动检测,所以完全可以当成计步器使用,计步器功能与小米手环早期的 米粒 比较类似,但算法应该是有非常大的区别的。

概述

这个小板取名 Sensor Mini 应该是很好理解的,比较传感器多,又比较迷你,这个 T 实在不知道怎么就脑子里突然闪现出这个字母,跟之前的 WatchT 共用了字母 T, 于是组合一下就是 SensorT Mini 了。

先说一下这个小板我个人认为比较优秀的部分:

  • 小巧,传感器完善;
  • 作为开发板,调试方便;
  • 2层板+0402超小封装,集成度还算可以;
  • 带显示屏,有锂电池充放电管理。

当然也有我觉得不够完善的地方,也是觉得最欠缺的地方:

  • 低功耗管理还不足,待机只有一个礼拜左右;

不过低功耗这个还是有原因,最开始打算使用 NRF51822 作为核心,同时也可以具备蓝牙功能,低功耗效果比目前芯片要好一些,但是因为原计划是想在这颗芯片上集成深度学习模型,考虑到F4系列的单片机都具备浮点加速功能,所以最终选择了 STM32F401CC 来作为核心,后续开发也证明选择是对的,但牺牲的就是功耗了。在频繁使用情况下待机可能只有2天,一把使用的情况下可以做到7天,偶尔使用的情况下可以做到 15~30 天。

设计

使用 Eagle PCB 进行全部设计。

SensorT Mini 与 WatchT

首先简单说下这个个人项目的由来。关注我的朋友应该知道前不久我做了一款 WatchT 的可编程智能手表(或称手环),WatchT 具备的功能包括:

  • 四个多功能用户按钮;
  • 毫秒定时器和秒表;
  • 基本的计时功能;
  • Oled 128×64单色屏显示;
  • 可编程的震动反馈;
  • 可编程的蜂鸣器;
  • 充电及电量百分比计算和显示;
  • 基于BMP180的气压海拔高度计;
  • 在BMP180基础之上开发的高度差传感器;
  • 实时温度传感器,可记录历史温度;
  • 一颗三轴加速度计,用于计步和运动识别;
  • 基于三轴加速度计的电子水平仪;
  • 可编程的游戏开发框架,基于此我编写了一个 flappy bird 游戏。

可以说这款DIY的手表如果可以非常省电的话,加上外壳就是一个很不错的户外运动手表了,但我觉得还不够完美,因为它还不够小,所以我在此基础上重新使用 STM32F401CC 作为核心芯片,画了一个更小的板子,也就是这个 SensorT Mini 了,取名 Sensor 是因为传感器比较丰富。

SensorT Mini 相对于 WatchT 做了如下改动:
– Oled 128×64 分辨率改为 128×32分辨率;
– BMP180气压传感器改为了BME280传感器,除了其他温度和气压之外,还增加了湿度功能,精度也更高,功耗也更低;
– 取消了震动反馈;
– 取消了蜂鸣器;
– 四个用户按钮改外三个,并将按钮改为波轮,节省了很大空间;

以上就是 SensorT Mini 相对于 WatchT 的主要改动,当然这只是硬件上的,软件上也要做一些适配,所以除了主要功能外,软件上取消了游戏开发框架,但增加了运动识别,运动识别是基于深度学习模型实时计算的,有关这部分我会在后面的文章中有简单介绍。

这是 SensorT Mini 焊接完成后的裸图:

这个小板依然使用了 STM32F401CC 作为主控芯片,集成了 CH340E (USB转串口芯片)、BME280 (温湿度气压传感器)和 LS3DH (三轴加速度计),充电芯片用的 TP4057

SensoT Mini 整体上还是非常小的,上图与一角钱硬币的对比照。

嵌入式编程

SensorT Mini的功能开发依然使用C++语言,借助 PlatformIO 和 MBed 平台,在VSCode上编程简直爽,这里简单介绍一下小板的软件框架。

程序在运行时,会创建两个主要进程,分别为:

  • GPIO等输入输出进行,如对按钮就行监测;
  • 显示进程,用于在OLED上绘制信息;

先看下main函数入库:

int main()
{
    sys_setup();
    threads_start();
    while (1)
    {
        if (sys_vars.enable_sleep && systimer.read() - sys_vars.last_action_time >= CONFIG_AUTO_SLEEP_AFTER) {
            lp_into_standby_mode();
        }
        thread_sys_display_fun();
    }
}

入口函数执行后,先进行系统的初始化,然后启动进程,最后进入while循环。在while循环中,程序会判断是否需要自动息屏,然后执行OLED显示程序。再来看OLED显示程序的主要工作:

void thread_sys_display_fun()
{
    u8g2_ClearBuffer(&myScreen);
    page_show_current();
    u8g2_SendBuffer(&myScreen);
}

可以看出,OLED的显示程序在每一帧的开头请显示缓存清空,然后绘制当前页面,最后把缓存的数据显示出来。这里面重要的就是 page_show_current 函数,该函数用来显示当前页面,那么怎么判断当前页面,也就是应用呢?继续看这个函数的实现:

void page_show_current()
{
    switch (sys_vars.page_id)
    {
    case PAGE_DEFAULT:
        page_default();
        break;
    case PAGE_MAIN_MENU:
        page_main_menu();
        break;
    case PAGE_APP_ALTITUDE:
        page_altitude();
        break;
    case PAGE_APP_TEMPERATUR:
        page_temperature();
        break;
    case PAGE_APP_HUMIDITY:
        page_humidity();
        break;
    case PAGE_APP_ACC:
        page_accelerometer();
        break;
    case PAGE_APP_GRADIENTER:
        page_gradienter();
        break;
    case PAGE_APP_SIMPLE_GAME:
        page_simple_game();
        break;
    case PAGE_APP_LED_TEST:
        page_led_test();
        break;
    case PAGE_APP_BTN_TEST:
        page_btn_test();
        break;
    case PAGE_APP_SPORTS:
        page_sports();
        break;
    case PAGE_ABOUT:
        page_about();
        break;
    default:
        page_default();
        break;
    }
}

可以看出来,page_show_current 函数通过一个系统结构体变量的 page_id 字段来判断当前调用哪个方法来进行页面渲染。这里高度计应用为例,当用户通过按钮将页面切换到 page_altitude 下时,通过判断 page_id 的值,此时会进行高度计页面的渲染,即执行 page_altitude 函数,这是该函数的定义:

void page_altitude()
{
    static sys_page_t thispage = {
        PAGE_APP_ALTITUDE,
        FPS_10,
        1,
        PAGE_FRAME_FIRST,
        0,
        PAGE_MAIN_MENU};
    static bool first = true;
    static float base_alit = 0;

    if (first)
    {
        page_set_fps(thispage.fps);
        sensor_bme_get_data(0.3);
        first = false;
        // 显示模式
        u8g2_SetFontMode(&myScreen, 1);
        u8g2_SetDrawColor(&myScreen, 1);
        // 禁止息屏
        sys_vars.enable_sleep = false;
    }
    // 显示信息
    u8g2_SetFont(&myScreen, u8g2_font_siji_t_6x10);
    u8g2_DrawStr(&myScreen, 0, 10, "<");
    u8g2_DrawGlyph(&myScreen, 8, 10, 0xe130 + 1);
    // 显示气压值
    char bmepressmsg[20];
    sprintf(bmepressmsg, "%.2f HPa", sys_sensor_bme_data.press);
    u8g2_DrawStr(&myScreen, 25, 10, bmepressmsg);

    // 显示基准海拔
    u8g2_SetFont(&myScreen, u8g2_font_5x7_tf);
    char baselevelmsg[20];
    sprintf(baselevelmsg, "%.1f M", base_alit);
    u8g2_DrawStr(&myScreen, 25, 22, "BASE");
    u8g2_DrawStr(&myScreen, 25, 30, baselevelmsg);

    // 显示相对高度
    u8g2_SetFont(&myScreen, u8g2_font_10x20_tf);
    char alitmsg[20];
    sprintf(alitmsg, "%5d", int(sys_sensor_bme_data.altitude - base_alit));
    u8g2_DrawStr(&myScreen, 128 - 60, 30, alitmsg);
    u8g2_SetFont(&myScreen, u8g2_font_7x13_tf);
    u8g2_DrawStr(&myScreen, 128 - 7, 30, "M");

    // 重设基准
    if (btn_ok_is_preassed())
    {
        base_alit = sys_sensor_bme_data.altitude;
    }
    // 清零
    if (btn_down_is_preassed())
    {
        base_alit = 0;
    }
    // 返回上一页
    if (btn_back_is_pressed())
    {
        base_alit = 0;
        first = true;
        page_back(thispage);
        return;
    }

    // 自增量,需页面内函数自行实现
    thispage.frame_count += 1;
    thispage.frame_index += 1;
    if (thispage.frame_index > thispage.fps)
    {
        thispage.frame_index = PAGE_FRAME_FIRST;
        // 重新获取bme传感器的值
        sensor_bme_get_data(0.3);
    }
}

可以看到这个页面在执行时定义了若干 static 静态变量,这是页面应用所需且必要的,因为页面会被循环调用,每次调用我们都希望页面能够之前它之前页面的运行时数据,所以这里规定每个页面可以在函数内定义自己的 static 变量。

值得注意的一点是,页面的顶部有一个 sys_page_t 的结构体变量,也是静态的,这个变量其实是页面的显示配置信息,定义了页面的刷新频率等必要信息。

以上是整个系统的运行框架,在这个框架内就可以定义出更多的页面,实现更多的功能,最后来在看张APP的运行时效果图吧。

原理图

上面是 SensorT mini 的原理图,喜欢的朋友可以自行下载画板打样制作,请酌情参考。

类似文章

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注