1 硬件介绍
1.1 控制器 NUCLIO-G0B1RE
性价比超高的STM32 Nucleo板,满足开发者对任意STM32 MCU新设计想法的尝试并可快速创建验证原型。该系列的开发板支持Arduino与ST Morpho扩展接口,方便开发者进行快速原型制作和应用测试,集成的ST-LINK调试器使得编程和调试过程简便快捷。STM提供了广泛的软件库、文档资源和社区支持帮助开发者最大化利用这些开发板。
1.2 外设
1.2.1 X-NUCLEO-IKS4A1
X-NUCLEO-IKS4A1是由搭载了传感器的主板IQS4A1和可拆卸的 Qvar 触摸/滑动电极附加板MKE001A组成,具有高度的集成性和兼容性,提供了在动作检测、环境监测等IoT领域的各种传感器解决方案。
1.2.2 USART1和USART2
工程中使用了两个串口进行通信,USART1用来实现与串口屏的通信,向串口屏发送指令,使触摸操作反馈在串口屏上,其中PC4是TX引脚,PC5是RX引脚,具体位置可以查询板卡和IKS4A1的原理图;USART2用来实现向上位机(此处为PC)发送数据,此串口通过板卡上的Micro-USB接口直接与电脑进行通信,只需配置USART2时选择PA2和PA3为TX和RX引脚即可。
1.2.3 串口屏(USART HMI)
第一次接触串口屏,对其了解其实并不多,特别是内部原理,简单介绍一下使用体验。串口屏相比LCD1602A和一般的OLED等我接触过的屏幕相比更强大的功能和更方便的设计方法。前者可以通过USART HMI程序直接进行可视化的界面设计,还可以进行更多的个性化设置,而后者需要使用坐标来控制显示内容的位置,而且自由度较低,想要创新的话非常困难。同时,前者拥有全彩等更好的显示效果,还支持触摸操作,非常适合实现本次任务的可视化。
1.3 应用方向
显然此款传感器为了便于开发设计的体积比较大,相信在真正的产品中可以做到较小的体积和更高的集成度,这样它就可以用于各种人机交互和姿态检测,比如提供VR头盔、手柄等的空间位置和移动轨迹,环境监测的各项参数在生产生活中也有着许多应用。
2 功能介绍
2.1 发送数据到上位机
在app_freertos.c中将PC_Send设置为01,然后通过NUCLEO-G0B1RE板卡上的Micro-USB接口将板卡与电脑连接,即可在电脑的串口助手上读取到传感器的数据信息。
2.2 在串口屏上可视化
将串口屏的RX引脚与PC4连接,TX与PC5连接,就可以在串口屏上显示传感器的数据,并且通过触摸控制某一项数据显示的开启与关闭,串口屏已经提前通过串口工具(如CH340)从PC上的USART HMI软件中下载了对应的程序。
如果没有串口屏,也可以通过CH340将PC4和PC5连接到PC上,在USART HMI中选择调试,选择左下角的用户MCU输入,即可“模拟”出一个串口屏。由此可以认为,串口屏其实就相当于一个上位机,它接受来自板卡的传感器数据并进行显示。
2.3 触摸控制逻辑
单击:移动
如果此刻选中的是某个传感器而不是具体的XYZ,那么单击将会按左右移动,右侧单击为向右移动选中目标,左侧单击为向左移动选中目标;如果已经选择了某个动作检测传感器,那么单击将移动选择的XYZ目标。
双击:开关 or 选择
如果此刻选中的是三个环境检测传感器或者某个动作传感器具体的XYZ,那么双击任意一侧将是开/关这个数据的显示;如果此刻选中的是某个动作传感器,那么双击任意一侧将进入XYZ的选择。PS:双击左右效果是一样的。
滑动: 翻页 or 退出
如果此刻选中的是某个传感器而不是具体的XYZ,那么滑动将进行翻页操作,串口屏一共有4页,第1页显示三个环境监测传感器数值,第2-4页分别显示三个动作检测传感器的数值;如果此刻已经选中某个动作传感器,正在选择XYZ,那么向任意方向滑动都是退出XYZ的选择,返回上一级传感器的选择。
3 设计思路
3.1 STM32CubeMX加载Demo
在STM32CubeMX中选择对应的芯片或板卡(比如我使用的G0B1RE),然后在Middleware and Software Pack选项中添加X-CUBE-MEMS1,在其中选择IKS4A1以及Application中的IKS4A1_DataLogFusion。
3.2 配置Demo所需的外设和引脚
在Middleware and Software Pack页面下方的Configuration中查看需要配置的信息,我们需要一个定时器,一个I2C,一个按钮引脚,一个USART以及一个LED引脚。在引脚配置中将这些外设配置好,并在Middleware and Software Pack中对应的位置填上外设信息就可以成功配置好Demo。
这个Demo可以在烧录成功之后可以让单片机通过指定的USART与电脑通信,从而将数据显示在MEMS Studio中。我们选用这个Demo主要是因为它帮助我们完成了各个传感器的初始化,我们可以直接调用读取数据的函数读取传感器的数据。但是任务的关键是如何实现触摸操作以及触摸逻辑和传感器数据的可视化。
3.3 下载QVAR驱动和Demo并移植
为了实现基于QVAR的触摸操作,我们需要加入QVAR的驱动。而QVAR是集成于LSM6DSV16X传感器的,因此我们可以去ST意法半导体官网直接搜索LSM6DSV16X,并在搜索结果的“工具与软件”中选择C-Driver-MEMS,点击获取软件后会跳转到一个Github页面(可以直接点这里 ),往下找到lsm6dsv16x_STdc,下载example文件夹里的“lsm6dsv16x_qvar_read_data_polling.c”文件即可。
下载完成后,我们只需把初始化的部分加入到我们的项目文件中即可。
3.4 配置FreeRTOS
由于一些操作需要延迟,一些操作不需要延迟,而且延迟的长短也不同,为了不阻塞程序运行,我选择了使用更便捷FreeRTOS。在STM32CubeMX中使用FreeRTOS需要在Middleware and Software Pack中加载FreeRTOS软件包,并且需要把“System Core”中“SYS”选项的“Timebase Source”从SysTick修改为其他未使用的定时器,因为FreeRTOS会占用滴答定时器,我们需要把系统定时基准源修改为其他定时器。
接下来在Middleware and Software Pack中FreeRTOS的配置界面即可创建自己的任务了。
3.5 设计串口屏界面
我使用的是陶晶驰公司的串口屏,串口屏的设计软件可以在陶晶驰官网中下载”USART HMI“软件进行自主设计。
3.6 实现单击、双击、滑动检测
在移植好QVAR读取Demo之后最好尝试一下读取是否正常,触摸两侧返回的电压值分别为420mV和-420mV,只有这两个数据可以被正确返回之后才能开展下一步的工作。接着,我们可以编写通过上升沿或者下降沿触发一次单击的代码,并通过串口测试是否可以正常触发单击。当单击可以正常识别之后我们可以添加双击和滑动的检测,比如我们可以检测两次单击之间的间隔是否小于某个值,如果小于则判定为双击或者滑动(PS:滑动可以看成不同测的双击)。
3.7 设计触摸逻辑
最后就是设计一套触摸逻辑了,如果已经实现了单击、双击和滑动检测,那么只需要配合自己的显示界面设计一套合理且易用的触摸逻辑。
4 软件流程图
5 主要代码说明
5.1 QVAR驱动移植
初始化部分可以直接复制,但是平台读写函数需要修改。
static int32_t platform_write(void *handle, uint8_t reg, const uint8_t *bufp,
uint16_t len)
{
HAL_I2C_Mem_Write(handle, LSM6DSV16X_I2C_ADD_L, reg,
I2C_MEMADD_SIZE_8BIT, (uint8_t *)bufp, len, 1000);
return 0;
}
static int32_t platform_read(void *handle, uint8_t reg, uint8_t *bufp,
uint16_t len)
{
HAL_I2C_Mem_Read(handle, LSM6DSV16X_I2C_ADD_H, reg,
I2C_MEMADD_SIZE_8BIT, bufp, len, 1000);
return 0;
}
只需要把官网下载的Demo中保留宏定义中带有NUCLEO_F401RE的部分保留,其他的可以直接删除。
请注意,platform_read函数中原本为LSM6DSV16X_I2C_ADD_L,需要修改为LSM6DSV16X_I2C_ADD_H,否则无法正确识别传感器的I2C地址
5.2 数据发送任务
void StartTask01(void *argument)
{
/* USER CODE BEGIN StartTask01 */
/* Infinite loop */
TickType_t ticks;
ticks = xTaskGetTickCount();
HMISendstart(); // 为确保串口HMI正常通信
sprintf(tjcstr, "temp.bco=57083");
HMISends(tjcstr);
HMISendb(0xff);
for (;;)
{
// 获取传感器数据
BSP_SENSOR_TEMP_GetValue(&temp_value);
BSP_SENSOR_HUM_GetValue(&hum_value);
BSP_SENSOR_PRESS_GetValue(&press_value);
BSP_SENSOR_ACC_GetAxes(&acc_value);
BSP_SENSOR_GYR_GetAxes(&gyr_value);
BSP_SENSOR_MAG_GetAxes(&mag_value);
#if Sensor_Send == 1
//向上位机(此处为电脑)发送传感器数据
printf("[温度] %.2f℃\r\n", temp_value);
printf("[湿度] %.2f%%\r\n", hum_value);
printf("[压强] %.2fmbar\r\n", press_value);
printf("[加速度] X:%dmg ; Y:%dmg ; Z:%dmg\r\n", acc_value.x,acc_value.y,acc_value.z);
printf("[陀螺仪] X:%dmdps ; Y:%dmdps ; Z:%dmdps\r\n", gyr_value.x,gyr_value.y,gyr_value.z);
printf("[磁场强度] X:%dmG ; Y:%dmG ; Z:%dmG\r\n", mag_value.x,mag_value.y,mag_value.z);
printf("\r\n");
#endif
if (temp_flag)
{
sprintf(tjcstr, "env_data.temp_value.txt=\"%.2f\"", temp_value);
HMISends(tjcstr);
HMISendb(0xff);
sprintf(tjcstr, "temp.pco=0");
HMISends(tjcstr);
HMISendb(0xff);
}
else
{
sprintf(tjcstr, "env_data.temp_value.txt=\"Empty\"");
HMISends(tjcstr);
HMISendb(0xff);
sprintf(tjcstr, "temp.pco=63488");
HMISends(tjcstr);
HMISendb(0xff);
}
//下面有很长一段向上位机(串口屏)发送数据的内容,为节约篇幅跳过
//······
vTaskDelayUntil(&ticks, 200);
}
/* USER CODE END StartTask01 */
}
这个任务在进入循环之前先对串口屏进行初始化,然后进入for循环。每次循环中先获取传感器数据,然后根据宏定义Sensor_Send是否为1确定是否向PC通过USART1发送传感器数据。接下来就是根据不同传感器对应标志位去判断是否向串口屏传输该传感器的数据,以及目前选中的传感器是哪一个。
5.3 QVAR数据获取任务
void StartTask02(void *argument)
{
/* USER CODE BEGIN StartTask02 */
/* Infinite loop */
for (;;)
{
/* Read output only if new values are available */
lsm6dsv16x_all_sources_get(&dev_ctx, &all_sources);
if (all_sources.drdy_ah_qvar)
{
lsm6dsv16x_ah_qvar_raw_get(&dev_ctx, &data);
QVAR_value = lsm6dsv16x_from_lsb_to_mv(data);
#if QVAR_Send == 1
sprintf((char *)tx_buffer, "QVAR(mV):%.0f\r\n", QVAR_value);
tx_com(tx_buffer, strlen((char const *)tx_buffer));
#endif
if (QVAR_value > 300 && (QVAR_value - QVAR_prv) > 100)
{
status = 1;
}
else if (QVAR_value < -300 && (QVAR_value - QVAR_prv) < -100)
{
status = 2;
}
if (status == 1)
{
status = 0;
time_now = xTaskGetTickCount();
if (time_now - time_prv > wait)
{
time_prv = time_now;
operation_ready = 1;
}
else if (time_now - time_prv <= wait)
{
time_prv = time_now;
operation_ready = 11;
}
}
if (status == 2)
{
status = 0;
time_now = xTaskGetTickCount();
if (time_now - time_prv > wait)
{
time_prv = time_now;
operation_ready = 2;
}
else if (time_now - time_prv <= wait)
{
time_prv = time_now;
operation_ready = 22;
}
}
}
osDelay(20);
QVAR_prv = QVAR_value;
}
/* USER CODE END StartTask02 */
}
这个任务先获取QVAR的电压数据,然后根据现在的电压值是否大于300(小于-300)来判断是否触摸在电容片上,再根据两次检测之间变化的值是否大于100(小于-100)来判断上升沿(下降沿),防止手指持续触摸在电容片上时多次触发按压,当检测到触摸时(相当于按钮的按下)就根据按压的是左还是右更新status变量的值。当status的值发生变化时就记录现在的时刻,并根据两次变化的时间差多少判断是否是双击或滑动的第二次点击(注意,这里还没有判断是双击还滑动,只是判断是短时间内的第一次点击还第二次),把结果反馈给operation_ready变量。
5.4 操作处理任务
void StartTask03(void *argument)
{
/* USER CODE BEGIN StartTask03 */
/* Infinite loop */
for (;;)
{
if (operation_ready == 1) // 操作预备值的修正
{
osDelay(wait);
switch (operation_ready)
{
case 1:
operation = 1;
break;
case 11:
operation = 11;
break;
case 22:
operation = 12;
break;
}
operation_ready = 0;
}
else if (operation_ready == 2)
{
osDelay(wait);
switch (operation_ready)
{
case 2:
operation = 2;
break;
case 22:
operation = 22;
break;
case 11:
operation = 21;
break;
}
operation_ready = 0;
}
if (operation)
{
switch (operation)
{
case 1:
if (layer == 1)
{
if (sensor > 1)
sensor -= 1;
else
sensor = 6;
}
else
{
if (xyz > 1)
xyz -= 1;
else
xyz = 3;
}
break;
case 2:
if (layer == 1)
{
if (sensor < 6)
sensor += 1;
else
sensor = 1;
}
else
{
if (xyz < 3)
xyz += 1;
else
xyz = 1;
}
break;
case 11:
if (layer == 1 && sensor >= 4)
{
layer += 1;
xyz = 1;
}
else
ON_OFF();
break;
case 22:
if (layer == 1 && sensor >= 4)
{
layer += 1;
xyz = 1;
}
else
ON_OFF();
break;
case 12:
if (layer == 2)
layer -= 1;
else
switch (sensor)
{
case 1:
case 2:
case 3:
sensor = 4;
break;
case 4:
sensor = 5;
break;
case 5:
sensor = 6;
break;
case 6:
sensor = 1;
break;
}
break;
case 21:
if (layer == 2)
layer -= 1;
else
switch (sensor)
{
case 1:
case 2:
case 3:
sensor = 6;
break;
case 4:
sensor = 1;
break;
case 5:
sensor = 4;
break;
case 6:
sensor = 5;
break;
}
break;
}
#if Operation_Send == 1
switch(operation)
{
case 1:
printf("左侧单击\r\n");
break;
case 2:
printf("右侧单击\r\n");
break;
case 11:
printf("左侧双击\r\n");
break;
case 22:
printf("右侧双击\r\n");
break;
case 12:
printf("向右滑动\r\n");
break;
case 21:
printf("向左滑动\r\n");
break;
}
#endif
operation = 0;
change = 1;
}
}
/* USER CODE END StartTask03 */
}
这个人任务是根据上个任务返回的操作预备值判断具体是哪一侧的单击、双击或者朝哪个方向的滑动,并根据触摸操作逻辑对串口屏显示内容进行相应的修改。具体的触摸操作逻辑在功能介绍中的触摸控制逻辑中查看。
6 心得体会
自从学习了使用STM32CubeMX创建工程我还没有真正应用过,这次活动为我提供了一个使用STM32CubeMX和HAL库的机会。同时,我也首次将FreeRTOS投入应用,体会到了RTOS的妙处。除此之外,我也练习了串口屏的使用,以任务目标为导向设计了串口屏的页面。而ST公司的传感器也让我大开眼界,MEMS Studio中对与传感器数据的处理也让我对数据融合有了新的见解,对我在嵌入式领域的学习也有极大益处。
同时,我也深刻体会到一个道理,任何知识和技能都要在实践中掌握,我们应当主动在实践中应用我们学到的知识和技能,特别是我们还不够熟练的知识与技能,就像我这次主动加入的FreeRTOS和串口屏。