项目背景
功能介绍
任务描述
- 使用板卡上的触摸按键,实现点按和左右滑动,实现传感器选择和切换
- 将数据发送到上位机
- 上位机完成传感器数据的功能选择和可视化
硬件介绍
运动和环境传感器拓展板
X-NUCLEO-IKS4A1 集成板配备了多种MEMS传感器,包括LSM6DSO16IS和LSM6DSV16X的3轴加速度和3轴角速度传感器、LIS2MDL三轴磁力计、LIS2DUXS12三轴加速计、LPS22DF压力传感器、STTS22H温度传感器和SHT40AD1B湿度温度传感器,能够在紧凑的空间内提供加速度、磁力、压力、温度等多维度测量。此外,板卡还配备了Qvar触摸/滑动电极,支持开发高度互动和响应式的应用。该板卡通过IIC协议与外界通讯,使得集成和应用开发更为便捷。
设计思路
主要困难
此小节主要分析功能需求,实现该项目会面临的困难。
从任务描述中可以看出,实现该任务有三个核心功能:
- 获取多种传感器数据并通过串口发送至上位机,此部分功能示例软件已经提供。
- 驱动触摸按键,获取用户触摸行为,将选择展示传感器信息传递给上位机。
- 上位机中传感器数据可视化的UI设计与显示逻辑。
解决思路
此小节针对面临的困难,提出解决方案(拟使用的传感器及通信协议)。
整体思路如图所示。
核心功能一
捕获用户点按和左右滑动的操作
主要使用传感器:MEMS 3D加速度计+ 3D陀螺仪(LSM6DSV16X)、其他运动传感器和环境传感器。
配置LSM6DSV16X的Qvar功能。打开X-NUCLEO-IKS4A1的文档,可以看到Qvar接口与传感器的连接方式
可以看到,可以通过 LSM6DSV16X使用装备的 Qvar 滑动电极。
板载配置为:使用跳线帽连接J4和J5的3-4端口
- J4: 3-4 (HUB1_SDx = QVAR1)
- J5: 3-4 (HUB1_Scx = QVAR2)
单片机iic,usart等外设初始化。传感器初始化,传感器采样时间为1s,并通过串口发送到上位机。
每隔0.1s获取qvar值,完成用户动作检测,当用户触发上滑,下滑,双击动作时,发送控制信号至上位机。
根据ST提供的传感器数据手册的描述,通过设置 CTRL7(16h)寄存器中的 AH_QVAR_EN 位为 1,可以激活 Qvar 通道;可以通过STATUS_REG (1Eh)的AH_QVARDA获取数据是否准备好;Qvar 数据以 16 位二进制补码形式提供,在 AH_QVAR_OUT_L(3Ah)和 AH_QVAR_OUT_H(3Bh)寄存器中以固定 240 Hz 的速率输出。
- 硬件连接 使用跳线帽连接J4和J5的3-4端口
- 配置传感器
阅读6.8 Qvar functionality章节,首先配置传感器功能,设置传感器内部寄存器的值。
通过CTRL1 (10h)设置加速度速率为120Hz,以达到加速度计为高性能的要求。
设置CTRL7(16h)寄存器中的 AH_QVAR_EN 位为 1。
- 获取Qvar数据
读取AH_QVAR_OUT_L(3Ah)和 AH_QVAR_OUT_H(3Bh)这两个寄存器,来获取Qvar 数据。
- 用户行为捕获
根据Qvar数据的变化特征,来判断用户是否在触摸板上完成手指的移动。采样Qvar的数据波形,识别用户是否开始点按和左右滑动。采样率也会导致识别行为的可靠性。向左滑动选择拟展示上一个传感器,向右滑动拟展示下一个传感器,双击后才切换显示所选择传感器的数据。
核心功能二
qt的ui设计,采用左右布局,左边为X-NUCLEO-IKS4A1板载传感器的名称,右边展示所选传感器的数据。左边布局可供用户鼠标点击,也可根据用户在开发套件中的行为更换样式。具体地,当用户在静电传感器上滑动时,按钮的样式会随着发生改变。电脑使用的COM口为COM9。
核心功能三
使用串口方式,开发板与上位机完成通信。主要传输传感器数据和用户行为。串口数据格式根据传感器种类不同分为二类,分别是命令类和数据类。
命令类为用户的上滑,下滑,双击等操作,主要用来控制上位机UI显示不同的传感器显示页面。
命令类格式为CMD:xx,其中,xx可能为up、down、double_click。
各种传感器的数据通过字符串发送,数据类格式如下:
Motion sensor instance 1:ACC_X: 15, ACC_Y: -20, ACC_Z: 1008
Motion sensor instance 1:GYR_X: 0, GYR_Y: 140, GYR_Z: 350
Motion sensor instance 2:ACC_X: -25, ACC_Y: -18, ACC_Z: 1010
Motion sensor instance 3:ACC_X: -21, ACC_Y: -25, ACC_Z: 1024
Motion sensor instance 3:GYR_X: 2590, GYR_Y: -3990, GYR_Z: -1960
Environmental sensor instance 0:Temp: +27.27 degC
Environmental sensor instance 1:Temp: +27.22 degC
Environmental sensor instance 1:Press: 1010.59 hPa
Environmental sensor instance 2:Hum: 43.87 %
Environmental sensor instance 2:Temp: +27.07 degC
运动传感器的字符串形如Motion sensor instance 0:MAG_X: -393, MAG_Y: -417, MAG_Z: -46
有效信息为传感器id,传感器类型(磁力计、加速度计、陀螺仪),三轴物理量数值
环境传感器的字符串形如Environmental sensor instance 0:Temp: +27.28 degC
有效信息为传感器id,传感器类型(温度、湿度、气压),环境物理量数值
上位机接收并通过正则表达式解析串口数据,控制UI页面的变化。
遇到的问题:停止串口接收时,上位机闪退,需要在串口数据接收前判断串口是否关闭。
软件流程图
此小节主要描述解决方案的软件实现,提出实现项目功能的技术路线。
上位机程序流程图
开发板程序流程图
功能展示
核心代码片段及说明
MCU核心代码
初始化外设
int main(void)
{
//...
MX_GPIO_Init();
MX_USART2_UART_Init();
MX_I2C1_Init();
MX_MEMS_Init();
}
初始化传感器
void MX_IKS4A1_DataLogTerminal_Init(void)
{
//...
IKS4A1_MOTION_SENSOR_Init(IKS4A1_LSM6DSV16X_0, MOTION_ACCELERO | MOTION_GYRO); // 1
IKS4A1_MOTION_SENSOR_Init(IKS4A1_LSM6DSO16IS_0, MOTION_ACCELERO | MOTION_GYRO); // 3
IKS4A1_MOTION_SENSOR_Init(IKS4A1_LIS2DUXS12_0, MOTION_ACCELERO); // 2
IKS4A1_MOTION_SENSOR_Init(IKS4A1_LIS2MDL_0, MOTION_MAGNETO); // 0
IKS4A1_ENV_SENSOR_Init(IKS4A1_SHT40AD1B_0, ENV_TEMPERATURE | ENV_HUMIDITY); // 2
IKS4A1_ENV_SENSOR_Init(IKS4A1_LPS22DF_0, ENV_TEMPERATURE | ENV_PRESSURE); // 1
IKS4A1_ENV_SENSOR_Init(IKS4A1_STTS22H_0, ENV_TEMPERATURE); // 0
//...
}
初始化Qvar传感器功能
/* Enable Qvar functionality */
lsm6dsv16x_ah_qvar_mode_t mode;
mode.ah_qvar_en = 1;
if (lsm6dsv16x_ah_qvar_mode_set(&(pObj->Ctx), mode) != LSM6DSV16X_OK)
{
return LSM6DSV16X_ERROR;
}
获取Qvar值
LSM6DSV16X_Object_t* pObj = LSM6DSV16X_GetQvar();
lsm6dsv16x_all_sources_get(&(pObj->Ctx), &all_sources);
if ( all_sources.drdy_ah_qvar ) {
lsm6dsv16x_ah_qvar_raw_get(&(pObj->Ctx), &qvar_data);
sprintf((char*)tx_buffer,"QVAR [mV]:%6.2f\r\n", lsm6dsv16x_from_lsb_to_mv(qvar_data));
printf((char*)tx_buffer);
}
用户点按和滑动行为捕获
每隔50ms判断Qvar值的变化,从而识别用户触摸状态
void MX_IKS4A1_DataLogTerminal_Process(void)
{
// 定义变量
static float_t t1 = 0;
static float_t t2 = 0;
static uint8_t t = 0;
static uint8_t t_click = 0;
static uint8_t t_sensor = 0;
static uint8_t flag_push = 0;
static uint8_t flag_click = 0;
static uint8_t flag_double_click = 0;
t1 = t2; // 获得上一个时刻Qvar的值
//...
t2 = lsm6dsv16x_from_lsb_to_mv(qvar_data);
// 状态判断
if (t2 < -50 && t1 > 50)
{
printf("CMD:up\r\n"); // 左滑
flag_push = 0;
}
else if (t2 > 50 && t1 < -50)
{
printf("CMD:down\r\n"); // 右滑
flag_push = 0;
}
else if ((-10 < t2 && t2 < 10) && (t1 < -50 || t1 > 50))
{
// printf("release\r\n");
t2 = 0; t1 = 0;
if (flag_click && flag_push && t_click < 10)
{
flag_double_click = 1;
printf("CMD:double_click\r\n"); // 双击
t = 0;
flag_click = 0;
}
else if(flag_push && t < 5) // 一秒内按下并抬起
{
flag_click = 1;
t_click = 0;
// printf("click\r\n");
}
flag_push = 0;
}
else if ((-10 < t1 && t1 < 10) && (t2 < -50 || t2 > 50))
{
// printf("push\r\n");
flag_push = 1;
t = 0;
}
// 1s发送一次传感器数据
if (t_sensor == 20){
t_sensor = 0;
//...
}
HAL_Delay( 50 );
t_sensor++;
t++;
t_click ++; // 从第一次点击后开始计时
if (t > 30){ // 长时间不点击,重置变量
t = 0;
t_click = 0;
flag_click = 0;
flag_double_click = 0;
flag_push = 0;
}
}
上位机核心代码(Python)
更新所选择传感器的按钮背景为黄色
# ...
def update_selected_button_styles(self, button_index):
self.selected_button_index = button_index
# 清除所有按钮的高亮样式
default_style = "QPushButton { border: 1px solid rgb(124, 124, 124); background-color: none; border-radius:2px;}"
for button in self.display_pushButtons:
button.setStyleSheet(default_style)
# 设置当前选中的按钮的高亮样式
active_style = "QPushButton { border: 1px solid rgb(124, 124, 124); background-color: yellow; border-radius:2px;}"
self.display_pushButtons[self.selected_button_index].setStyleSheet(active_style)
self.current_button_index = self.selected_button_index
self.stackedWidget.setCurrentIndex(button_index)
更新待选择传感器的按钮边框为虚线
# ...
def update_current_button_styles(self):
default_style = "QPushButton { border: 1px solid rgb(124, 124, 124); background-color: none; border-radius:2px;}"
if self.same_flag:
active_style = "QPushButton { border: 1px solid rgb(124, 124, 124); background-color: yellow; border-radius:2px;}"
self.display_pushButtons[self.selected_button_index].setStyleSheet(active_style)
self.same_flag = 0
for i, button in enumerate(self.display_pushButtons):
if i == self.current_button_index:
if self.current_button_index == self.selected_button_index:
active_style = "QPushButton {border: 1px dashed #000000; background-color: yellow;}"
self.same_flag = 1
else:
active_style = "QPushButton {border: 1px dashed #000000; background-color: none;}"
button.setStyleSheet(active_style)
elif i != self.selected_button_index:
button.setStyleSheet(default_style)
开启串口接收并更新传感器数值
电脑使用的COM口为COM9,根据实际情况进行修改
def start_serial(self):
self.serial_manager = SerialManager(com="COM9")
self.serial_manager.command_received.connect(self.update_cmd_display)
self.serial_manager.env_data_received.connect(self.update_env_display)
self.serial_manager.motion_data_received.connect(self.update_motion_display)
self.serial_manager.start()
def update_env_display(self, sensor_id, sensor_type, data_value):
if sensor_id == 0: # STTS22H button 7
if sensor_type == "Temp":
self.textEdit7_1.setText(str(data_value))
elif sensor_id == 1: # LPS22DF button 6
if sensor_type == "Temp":
self.textEdit6_1.setText(str(data_value))
elif sensor_type == "Press":
self.textEdit6_2.setText(str(data_value))
# ...
使用正则表达式解析关键信息
def run(self):
pattern_motion = r"Motion sensor instance (\d+):([A-Z]+)_X: (-?\d+), ([A-Z]+)_Y: (-?\d+), ([A-Z]+)_Z: (-?\d+)"
pattern_env = r"Environmental sensor instance (\d+):([A-Za-z]+): ([\+\-\d\.]+) ([a-zA-Z%]+)"
self.ser = serial.Serial(self.COM, baudrate=115200, timeout=1, parity=serial.PARITY_NONE, rtscts=0)
self.sio = io.TextIOWrapper(io.BufferedRWPair(self.ser, self.ser))
self.running = True
while self.running and self.sio:
line = self.sio.readline().strip() # Remove leading/trailing whitespace
if line.startswith("CMD:"):
data = line[4:]
self.command_received.emit(data)
else:
# match_env = re.search(pattern_env, line)
match_env = re.findall(pattern_env, line)
if match_env:
sensor_id = match_env[0][0]
sensor_type = match_env[0][1]
data_value = match_env[0][2]
self.env_data_received.emit(int(sensor_id), sensor_type, float(data_value))
else:
match_motion = re.findall(pattern_motion, line)
if match_motion:
sensor_id = match_motion[0][0]
sensor_type = match_motion[0][1]
values = [int(match_motion[0][2]), int(match_motion[0][4]), int(match_motion[0][6])]
self.motion_data_received.emit(int(sensor_id), sensor_type, values)
实现效果
上位机的显示布局效果如下图所示。
用户有两种途径切换不同传感器的数据展示页面,一是直接在上位机中点击相应按钮,而是通过左右滑动和双击静电传感器来选择。当用户左右滑动时,待选定的按钮样式发生变化,按钮的外边框为虚线,当用户双击时,按钮背景变黄,并在右边页面显示相关传感器数据。
UI中显示了以下关键信息
- 运动和环境传感器名称
- 各种物理量的测量数据
- 开启和关闭串口的按钮
- 按钮的样式可代表当前选择的传感器和待选定的传感器。
可实现开启或关闭串口功能,具体测试效果可观看演示视频。
总结
本项目依托X-NUCLEO-IKS4A1和NUCLEO-G0B1平台,完成交互式传感器数据可视化功能。在此过程中阅读传感器数据手册和平台驱动代码,分别编写MCU端的驱动代码和上位机的UI逻辑代码,完成串口通信和数据解析,最终顺利完成任务。