一、项目简介
本项目基于NUCLEO-G0B1RE和X-NUCLEO-IKS4A1开发板,通过点按和滑动Qvar触摸电极来选择和切换不同的传感器,采集加速度、温度、气压等环境数据。采集到的数据通过串口发送至PC上位机,并利用Electron框架构建的界面进行实时可视化展示,从而实现对动作和环境的动态监测。
二、硬件简介
NUCLEO-G0B1RE是一款基于STM32G0B1RE微控制器的开发板,属于STMicroelectronics的Nucleo系列,旨在为开发者提供便捷、高效的开发环境。该开发板具有以下主要特性:
- 核心微控制器:STM32G0B1RE,基于ARM Cortex-M0+内核,具有低功耗和高性能的特点。
- 集成调试器:板载ST-Link/V2-1调试器,支持编程、调试和虚拟COM端口通信,无需额外的调试工具。
- 易于使用:支持广泛的开发环境,包括STM32CubeIDE、IAR EWARM、Keil MDK和第三方开发工具,适合初学者和专业开发者快速上手。
NUCLEO-G0B1RE 开发板的设计旨在帮助开发者快速验证新设计并创建原型,广泛应用于物联网、工业控制、消费电子等领域。
X-NUCLEO-IKS4A1是是一款功能丰富的传感器扩展板,专为与STM32 Nucleo开发板配合使用而设计。该板集成了多种传感器,适用于广泛的应用场景,包括运动检测和环境监控。主要特性包括:
- 集成传感器:3D加速度计、陀螺仪、磁力计、气压计、温湿度传感器。
- 可拆卸Qvar触摸/滑动电极模块(STEVAL-MKE001A):提供点按和滑动触摸功能,增强了用户交互体验。
- 兼容性:与多种STM32 Nucleo开发板兼容,通过I2C或SPI接口进行通信,方便开发者进行快速原型设计。
X-NUCLEO-IKS4A1 使得开发者能够轻松集成和使用多种传感器,为物联网和嵌入式系统项目提供了强大的硬件基础。
三、软件简介
软件概述
这是一款基于Electron框架开发的PC端上位机软件,主要用于接收并显示从NUCLEO-G0B1RE开发板通过串口发送的X-NUCLEO-IKS4A1传感器数据。软件提供了直观的用户界面,能够实时监控并显示多个传感器的数据,包括加速度计、陀螺仪、磁力计、温湿度传感器和压力传感器的数据。
功能特点
- 串口通信:
- 支持多种波特率选择(9600、19200、38400、57600、115200)。
- 动态列出可用串口,并允许用户选择串口进行连接。
- 提供串口打开和关闭功能,并实时显示连接状态。
- 数据显示:
- 实时接收并解析传感器数据。
- 以表格形式显示加速度计、陀螺仪、磁力计、温湿度传感器和压力传感器的数据。
- 支持点击传感器标题,切换显示状态。
- 日志记录:
- 显示实时接收到的串口数据日志,便于调试和监控数据变化。
- 提供清空数据功能,方便用户重新监控。
技术实现
- 基于Electron框架:实现跨平台桌面应用,支持Windows、macOS和Linux系统。
- SerialPort库:用于实现串口通信,支持多种串口操作和数据处理。
- HTML/CSS/JavaScript:前端界面开发,提供用户友好的交互界面和实时数据展示。
使用说明
- 打开软件后,在控制面板中选择串口和波特率。
- 点击“打开”按钮,连接到NUCLEO-G0B1RE开发板。
- 实时查看并监控从X-NUCLEO-IKS4A1传感器板发送的数据。
- 左右滑动QVAR电极模块切换选中传感器数据,单击模块右侧关闭选中数据显示,单击模块左侧开启选中数据显示。
- 可通过鼠标点击传感器标题切换数据的显示状态,或点击“清空数据”按钮清空当前数据和日志。
- 点击“关闭”按钮断开串口连接。
代码实现
更新日志框的函数
function updateLogBox(data) {
const logBox = document.getElementById('log-box');
logBox.textContent += data + '\n';
logBox.scrollTop = logBox.scrollHeight;
}
- 将数据追加到日志框,并自动滚动到最新日志位置。
打开/关闭串口按钮事件监听器
document.getElementById('open-port').addEventListener('click', () => {
const portName = document.getElementById('port').value;
const baudRate = parseInt(document.getElementById('baudrate').value, 10);
ipcRenderer.send('open-port', { portName, baudRate });
updateConnectionStatus(true); // 假设连接成功
});
document.getElementById('close-port').addEventListener('click', () => {
ipcRenderer.send('close-port');
updateConnectionStatus(false); // 假设连接已关闭
});
- 绑定打开和关闭串口按钮的点击事件,发送对应的IPC消息,并更新连接状态。
清除数据按钮事件监听器
document.getElementById('clear-data').addEventListener('click', clearAllData);
- 绑定清除数据按钮的点击事件,调用
clearAllData
函数清空所有数据。
IPC 事件监听器
ipcRenderer.on('serial-data', (event, data) => {
console.log('Received data:', data);
dataBuffer += data;
if (dataBuffer.includes('\n')) {
const completeData = dataBuffer.split('\n');
dataBuffer = completeData.pop(); // 剩余的部分保留在缓冲区中
completeData.forEach(dataLine => {
console.log('Processing line:', dataLine);
const parsedData = parseSerialData(dataLine);
console.log('Parsed data:', parsedData);
updateSensorDisplay();
updateDisplay(parsedData);
updateLogBox(dataLine);
});
}
});
ipcRenderer.on('serial-error', (event, error) => {
console.error(`Error: ${error}`);
});
ipcRenderer.on('ports-list', (event, ports) => {
const portSelect = document.getElementById('port');
portSelect.innerHTML = '';
ports.forEach((port) => {
const option = document.createElement('option');
option.value = port.path;
option.textContent = port.path;
portSelect.appendChild(option);
});
});
ipcRenderer.send('list-ports');
- 监听
serial-data
事件接收串口数据,处理并更新显示。 - 监听
serial-error
事件处理错误。 - 监听
ports-list
事件列出可用串口。
更新连接状态的函数
function updateConnectionStatus(connected) {
const statusElement = document.getElementById('connection-status');
statusElement.textContent = connected ? '已连接' : '未连接';
statusElement.style.color = connected ? '#00e676' : '#ff4081';
}
- 更新连接状态的显示,改变文本和颜色。
清除所有数据的函数
function clearAllData() {
const sensorIds = [
'lsm6dsv16x-acc-x', 'lsm6dsv16x-acc-y', 'lsm6dsv16x-acc-z',
'lsm6dsv16x-gyr-x', 'lsm6dsv16x-gyr-y', 'lsm6dsv16x-gyr-z',
'lis2mdl-mag-x', 'lis2mdl-mag-y', 'lis2mdl-mag-z',
'environment-humidity', 'environment-temperature', 'environment-pressure'
];
sensorIds.forEach(id => {
document.getElementById(id).textContent = '-';
});
document.getElementById('log-box').textContent = '';
}
- 清空所有传感器数据和日志框内容。
解析串口数据的函数
javascript复制代码function parseSerialData(data) {
const dataObj = {};
// 匹配数据的正则表达式
const pressMatch = data.match(/PressValue: PRE = (\d+)/);
const tempMatch = data.match(/TempValue: TEM = (\d+)/);
const humMatch = data.match(/HumValue: HUM = (\d+)/);
const accXMatch = data.match(/AccValue: ACC.X = (-?\d+)/);
const accYMatch = data.match(/AccValue: ACC.Y = (-?\d+)/);
const accZMatch = data.match(/AccValue: ACC.Z = (-?\d+)/);
// 根据匹配结果填充 dataObj 对象
if (pressMatch) dataObj['PRESSURE'] = pressMatch[1] / 100;
if (tempMatch) dataObj['TEMPERATURE'] = tempMatch[1] / 100;
if (humMatch) dataObj['HUMIDITY'] = humMatch[1] / 100;
if (accXMatch) dataObj['ACCELERATION_X'] = accXMatch[1];
if (accYMatch) dataObj['ACCELERATION_Y'] = accYMatch[1];
if (accZMatch) dataObj['ACCELERATION_Z'] = accZMatch[1]; //省略重复代码
return dataObj;
}
- 解析接收到的串口数据,将其转换为对象格式。
更新显示数据的函数
function updateDisplay(data) {
if (data['OFF_ON']) { //根据OFF_ON修改显示状态
if (data['OFF_ON'] === '2') {
displayStatus[Cur] = 0;
} else if (data['OFF_ON'] === '1') {
displayStatus[Cur] = 1;
}
}
if (data['Cur_select']) { //更新当前选中数据
Cur = data['Cur_select'];
updateChooseDisplay();
}
if (data['ACCELERATION_X']) document.getElementById('lsm6dsv16x-acc-x').textContent = data['ACCELERATION_X']; //省略重复代码
}
- 根据解析后的数据更新显示内容。
更新传感器显示的函数
function updateSensorDisplay() {
const sensorIds = [
'lsm6dsv16x-acc-x', 'lsm6dsv16x-acc-y', 'lsm6dsv16x-acc-z',
'lsm6dsv16x-gyr-x', 'lsm6dsv16x-gyr-y', 'lsm6dsv16x-gyr-z',
'lis2mdl-mag-x', 'lis2mdl-mag-y', 'lis2mdl-mag-z',
'environment-humidity', 'environment-temperature', 'environment-pressure'
];
sensorIds.forEach((id, index) => {
const element = document.getElementById(id);
if (displayStatus[index] === 0) {
element.style.color = '#333333'; // 文字颜色设置为与背景相同
} else {
element.style.color = '#00e676'; // 显示
}
});
}
- 根据
displayStatus
数组的状态显示或隐藏传感器数据。
更新当前选择显示的函数
function updateChooseDisplay() {
const titleIds = [
'acc-x-title', 'acc-y-title', 'acc-z-title',
'gyr-x-title', 'gyr-y-title', 'gyr-z-title',
'mag-x-title', 'mag-y-title', 'mag-z-title',
'humidity-title', 'temperature-title', 'pressure-title'
];
titleIds.forEach((id, index) => {
const titleElement = document.getElementById(titleIds[index]);
if (Cur == index) {
titleElement.style.color = '#CCFFFF'; // 设置标题颜色
} else {
titleElement.style.color = '#ff4081'; // 恢复默认标题颜色
}
});
}
- 根据当前选择的传感器索引更新标题颜色。
切换文本颜色的函数
function toggleTextColor(element) {
const currentColor = element.style.color;
if (currentColor === 'rgb(255, 64, 129)' || currentColor === '#ff4081') {
element.style.color = '#CCFFFF';
switch (element.id) {
case 'lsm6dsv16x-title':
displayStatus[0] = 0; displayStatus[1] = 0; displayStatus[2] = 0;
displayStatus[3] = 0; displayStatus[4] = 0; displayStatus[5] = 0;
break;
case 'lis2mdl-title':
displayStatus[6] = 0; displayStatus[7] = 0; displayStatus[8] = 0;
break;
case 'sht40ad1b-title':
displayStatus[9] = 0; displayStatus[10] = 0;
break;
case 'lps22df-title':
displayStatus[11] = 0;
break;
default:
break;
}
} else {
element.style.color = '#ff4081';
switch (element.id) {
case 'lsm6dsv16x-title':
displayStatus[0] = 1; displayStatus[1] = 1; displayStatus[2] = 1;
displayStatus[3] = 1; displayStatus[4] = 1; displayStatus[5] = 1;
break;
case 'lis2mdl-title':
displayStatus[6] = 1; displayStatus[7] = 1; displayStatus[8] = 1;
break;
case 'sht40ad1b-title':
displayStatus[9] = 1; displayStatus[10] = 1;
break;
case 'lps22df-title':
displayStatus[11] = 1;
break;
default:
break;
}
}
}
- 切换传感器标题的文本颜色并更新对应的显示状态。
数据采集部分概述
数据采集部分采用STM32CubeIDE开发。首先搜索STM32G0B1选择NUCLEO-G0B1RE开发板创建工程。配置并开启I2C1,配置管脚为PB9PB8。安装和配置X-CUBE-MEMS1软件包,在Software Packs中安装X-CUBE-MEMS1软件包,在Middleware and Software Packs中配置X-CUBE-MEMS1。本部分详细内容推荐看ZERO大佬的文章。
IKS4A1读取传感器数值教程 - 知乎 (zhihu.com)
基于IKS4A1的QVAR电极的触摸识别的思路与实现 - 知乎 (zhihu.com)
代码实现
传感器数据采集发送
uint32_t lastTick = 0; // 用于记录上一次执行任务的时间
uint32_t currentTick = HAL_GetTick();
if (currentTick - lastTick >= 500) { //500ms采集并发送一次数据
MX_MEMS_Process();//添加至主循环
lastTick = currentTick; // 更新lastTick为当前时间
}
- 根据当前选择的传感器索引更新标题颜色。
QVAR数据处理和控制命令发送
int16_t value;
BSP_SENSOR_QVAR_GetValue(&value); //获取QVAR的数值
//函数具体细节见ZERO大佬的文章
qvar_digital(value, &act_output); // 调用 qvar_digital 函数处理触摸按键值
switch (act_output) {// 根据 act_output 的值执行相应的动作
case -2:
printf("OFF_ON: Val = %d\n", 2); //隐藏当前选中数据
break;
case -1:
if (++Cur > 11)
Cur = 0;
printf("Cur_select: Cur = %d\n", Cur); //切换选中数据
break;
case 0:
// 没有动作或者动作未被识别
break;
case 1:
if (--Cur < 0)
Cur = 11;
printf("Cur_select: Cur = %d\n", Cur); //切换选中数据
break;
case 2:
printf("OFF_ON: Val = %d\n", 1); //显示当前选中数据
break;
default:
// 其他未定义的动作
break;
}
HAL_Delay(1);
- 获取并处理QVAR值,向上位机发送控制命令。
可编译下载的代码
链接:https://pan.baidu.com/s/1Sxd2JBMpJZe7-LNz3iTraA
提取码:C6C6
四、总结与展望
总结
在此次Funpack活动中,我收获颇丰,具体总结如下:
- 掌握了通过STM32CubeIDE开发STM32:
- 学会了如何配置和使用STM32CubeIDE开发环境。
- 能够熟练应用ST官方提供的软件包进行传感器数据的读取和处理。
- 学会了使用ST官方提供的软件包:
- 理解并掌握了X-CUBE-MEMS1软件包的配置和使用方法。
- 成功实现了通过触摸按键选择和切换传感器,并将数据发送到上位机。
- 掌握了通过Electron框架开发PC上位机:
- 使用Electron框架构建了上位机界面,实现了传感器数据的接收和处理。
- 能够将MCU发送的数据在上位机上进行可视化展示。
- 学会了基础的视频剪辑:
- 完成了任务所需的3-5分钟短视频制作,学习并掌握了基础的视频剪辑和AI配音技巧。
展望
- 完善上位机:
- 目前上位机中仍存在一些BUG(如需要先将STM32连接到PC再启动上位机),有待进一步调试和修复。
- 计划构建更美观、更加用户友好的上位机界面,提升整体用户体验。
- 尝试实现任务二和三:
- 活动结束后,我将参考其他朋友们的项目,尝试实现任务二和任务三,进一步提升自己的技能和经验。
此次活动不仅让我在实践能力上有了显著的提升,更加深了我对电子设计和物联网开发的热情和兴趣。未来,我将继续努力,争取在这些领域取得更大的进步和成长。