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 的原理图,喜欢的朋友可以自行下载画板打样制作,请酌情参考。