这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 【Let'sdo2026第3期】NanoEdgeAIStudio识别电机状态

共1条 1/1 1 跳转至页

【Let'sdo2026第3期】NanoEdgeAIStudio识别电机状态

菜鸟
2026-11-03 00:12:04     打赏

本篇文章将从电机控制、加速度计 FIFO 数据采集、环境配置、模型训练与部署,介绍如何在 NUCLEO-H533RE 开发板上配合 NanoEdge AI Studio 在由 LIS3DH 采集的数据集上训练的电机运行状态分类应用,代码已经开源到 Git 仓库 https://codeberg.org/scgummy/eepw-letsdo-2026-p3image_1793627162307_0.png

Zephyr 驱动步进电机

Zephyr 下,我们可以用官方的 Stepper API 来操作步进电机,为了接入 Stepper API,我们仍然像之前一样需要用设备树描述我们的 4 根驱动线.

这次活动使用的 Seeed Studio 的步进电机套装中配套了一个驱动板,可以看到引出有 4 线输入,GND / VCC / VM,电机是 5V DC 步进电机.

image_1793202730200_0.png

原理图如下,可以看到除外围配套电路之外,其实驱动板的灵魂部件就是 ULN2003A.

image_1793202638501_0.png

ULN2003A 内部是 7 路 Darlington 晶体管阵列,每路有输入电阻和续流二极管,适合驱动继电器、电磁阀、步进电机这类感性负载. 用在 28BYJ-48 这类四相步进电机时,它的功能是接入 MCU 的 4 个 GPIO 输出相序后,把 GPIO 的小电流逻辑信号放大成绕组电流;驱动类型为低侧灌电流驱动,即电机公共端接电源正,ULN2003 输出导通时把绕组拉到地;内置续流二极管可以吸收绕组断电时的反电动势.

使用的 Zephyr 4.3.1 版本中,这类直接通过 GPIO 时序驱动的步进电机已经被并入 H 桥电机的驱动了,虽然这里并不是 H 桥,但是驱动层面 Zephyr 做了合并,老版本的 Zephyr 是分离,可能对使用不同 Zephyr 版本的开发者造成一定困惑.

对照我们的扩展板原理图,用设备树配置我们的步进电机端口.

image_1793628809536_0.png

image_1793628754229_0.png

然后可以实现用串口控制步进电机,包含三种模式:停转、慢转和快转. 慢转对应字符 1 或者 S,快转对应字符 2 或者 F,其它情况则停转.

image_1793628939274_0.png

加速度 FIFO 读取

前一篇帖子读出了加速度计数据并显示在 OLED 上,但这里使用的是 Zephyr 的 Sensor API 读取的,它只暴露最新的值. 在电机状态识别这类场景下,我们采集的加速度值的时间序列数据必须是前后间隔一致,完全连续采集的模式,否则特征提取阶段就会出现问题.

oled_1792386966602_0.gif

大多数 MEMS 的 IMU 都有 ODR 和环形 FIFO,ODR 决定采多快,环形 FIFO 则把已采数据暂存下来,但如果数据消费者(控制器侧,一般是 MCU)没有来得及取,新数据就会从队列开头覆盖老数据.

image_1793616654178_0.png

本次选择的 LIS3DH 模块片上带深度为 32 的 FIFO,同时提供不同的 FIFO 模式,因为我们是连续采集,所以我们将配置为 stream 模式.

image_1793616780476_0.png

打开 FIFO 涉及到 2 个具体的寄存器,分别是 FIFO_CTRL 和 CTRL5,前者包含一个 FM 字段可以选择 FIFO 模式,后者 CTRL5 包含 FIFO 的总开关,不打开 FIFO 就不会装入采集的数据.

image_1793617017915_0.png

image_1793616997010_0.png

落实到代码上,需要用 Zephyr 的 I2C reg_write_byte / reg_read_byte 系列函数进行操作,此外因为 Zephyr 本身就会初始化传感器的不少寄存器,防止打架,我们先读取已经有的值再用 bitops 去修改具体的字段.

int lis3dh_fifo_enable(const struct i2c_dt_spec *spec)
{
	int ret;

	uint8_t reg;

	ret = i2c_reg_read_byte_dt(spec, LIS3DH_REG_FIFO_CTRL, &reg);
	if (ret < 0) {
		return ret;
	}

	reg &= ~LIS3DH_FM_MASK;
	reg |= LIS3DH_FM_SELECT(LIS3DH_FM_STREAM);

	ret = i2c_reg_write_byte_dt(spec, LIS3DH_REG_FIFO_CTRL, reg);
	if (ret < 0) {
		return ret;
	}

	ret = i2c_reg_read_byte_dt(spec, LIS3DH_REG_CTRL5, &reg);
	if (ret < 0) {
		return ret;
	}

	reg |= LIS3DH_FIFO_EN(1);

	ret = i2c_reg_write_byte_dt(spec, LIS3DH_REG_CTRL5, reg);
	if (ret < 0) {
		return ret;
	}

	return 0;
}

然后怎么流式读取 LIS3DH 上的数据呢?简单来说,直接从 0x28 这个 OUT_X_L 寄存器开始发起 I2C burst read,连续读 6 个字节就是 1 组样本,连续读 12 个字节就是 2 个样本,设备会自动按照 0x28 0x29 0x2a 0x2b 0x2c 0x2d 的递进并从头循环.

image_1793617711434_0.png

落实到 Zephyr 代码上,我们对外提供一个 fifo_read 的函数,用户可以提供样本缓冲区的大小 count,我们将其跟 FIFO 中可用数据量(单位是 1 组样本)比较,取较小值作为我们实际读取的量,数值转换方面我们是从 Zephyr 官方库中复用的函数.

ssize_t lis3dh_fifo_read(
	const struct i2c_dt_spec *spec,
	int accel_fs,
	struct sensor_accel *buf,
	size_t count
)
{
	int ret;

	uint8_t src;
	size_t total;
	size_t done = 0;

	ret = i2c_reg_read_byte_dt(spec, LIS3DH_REG_FIFO_SRC, &src);

	if (ret < 0) {
		return ret;
	}

	total = src & LIS3DH_FSS_MASK;

	if (count < total) {
		total = count;
	}

	while (done < total) {
		uint8_t chunk[LIS3DH_FIFO_CHUNK_SAMPLES * LIS3DH_FIFO_SAMPLE_SIZE];
		size_t n = total - done;

		if (n > LIS3DH_FIFO_CHUNK_SAMPLES) {
			n = LIS3DH_FIFO_CHUNK_SAMPLES;
		}

		ret = i2c_burst_read_dt(spec, LIS3DH_REG_OUT_X_L | 0x80, chunk, n * LIS3DH_FIFO_SAMPLE_SIZE);

		if (ret < 0) {
			return ret;
		}

		for (int i = 0; i < n; i++) {
			const uint8_t *p = &chunk[i * LIS3DH_FIFO_SAMPLE_SIZE];
			struct sensor_accel *target = &buf[done + i];
			lis3dh_convert((int16_t)sys_get_le16(&p[0]), ACCEL_SCALE(1600), &target->xyz[0]);
			lis3dh_convert((int16_t)sys_get_le16(&p[2]), ACCEL_SCALE(1600), &target->xyz[1]);
			lis3dh_convert((int16_t)sys_get_le16(&p[4]), ACCEL_SCALE(1600), &target->xyz[2]);
		}

		done += n;
	}

	return (ssize_t)done;
}

数据采集和处理

我们在 Zephyr 上编写了一段采集函数,初始化后就执行,5 秒内如果串口上有数据就跳过采集模式直接回到主程序. 数据交换格式采用自定义输出,走 UART,一条数据类似下方这种格式.

@TAG(x1,x2),(y1,y2),(z1,z2)#00

其中 TAG 合法值有 X S F,分别代表停止、慢转和快转,后面紧跟三轴数据,x1 和 x2 分别对应 Zephyr 定义的 sensor_value 结构体中 val1 和 val2 的值,其本质上是一种定点数表示,真实值 v = val1 + val2 * 1e-6.

static int app_collect(struct app *app)
{
	int ret;

	unsigned char c;

	enum app_collect_state {
		STATE_STOP = 'X',
		STATE_SLOW = 'S',
		STATE_FAST = 'F',
	} state = STATE_STOP;

	int64_t last;

#define COOLDOWN      5
#define STATE_KEEP    (15 * 1000) // 15 secs
#define SLOW_INTERVAL 1464844
#define FAST_INTERVAL 732422

	for (int i = 0; i < COOLDOWN; i++) {
		ret = uart_poll_in(app->uart, &c);
		if (ret == 0) {
			return -1;
		}

		ret = cfb_framebuffer_clear(app->oled, false);
		ret = cfb_print(app->oled, "Press any key to exit sampler...", 0, 0);
		ret = cfb_framebuffer_finalize(app->oled);

		k_sleep(K_MSEC(1000));
	}

	ret = cfb_framebuffer_clear(app->oled, false);
	ret = cfb_print(app->oled, "Collecting", 0, 0);
	ret = cfb_framebuffer_finalize(app->oled);


	k_sleep(K_MSEC(1000));
	stepper_enable(app->stepper);

	last = 0;

	while (1) {
		size_t n;
		struct sensor_accel accel[16];

		ret = app_fetch_accel_fifo(app, accel, ARRAY_SIZE(accel));

		if (ret < 0) {
			printk("Failed to read accel: %d.\n", ret);
			return ret;
		}

		n = ret;

		for (int i = 0; i < n; i++) {
			char tag = (char)state;
			printf(
				"@%c(%d,%03d),(%d,%03d),(%d,%03d)#%02d\n",
				tag,
				accel[i].xyz[0].val1, accel[i].xyz[0].val2,
				accel[i].xyz[1].val1, accel[i].xyz[1].val2,
				accel[i].xyz[2].val1, accel[i].xyz[2].val2,
				i
			);
		}

		if ((k_uptime_get() - last) >= STATE_KEEP) {
			enum app_collect_state old = state;

			stepper_stop(app->stepper);

			k_sleep(K_MSEC(10));

			switch (old) {
			case STATE_STOP:
				state = STATE_SLOW;
				stepper_set_microstep_interval(app->stepper, SLOW_INTERVAL);
				stepper_run(app->stepper, STEPPER_DIRECTION_POSITIVE);
				break;
			case STATE_SLOW:
				state = STATE_FAST;
				stepper_set_microstep_interval(app->stepper, FAST_INTERVAL);
				stepper_run(app->stepper, STEPPER_DIRECTION_POSITIVE);
				break;
			case STATE_FAST:
				state = STATE_STOP;
				stepper_stop(app->stepper);
				break;
			}

			last = k_uptime_get();
			printf("State: %c -> %c\n", old, state);
		}

		k_sleep(K_MSEC(5));
	}

	return 0;
}

UART 用 115200 baud/s 可能太慢了,我们直接开到 921600 baud/s 这样可以确保 FIFO 来得及拿数据.

image_1793622568363_0.png

采集得到的原始输出是.

image_1793621412631_0.png

这样的格式方便最快的输出,但是 NanoEdge AI Studio 是不认的,我们用一个 Python 脚本将其转化成 NEAI Studio 识别的格式,转化成每一行 256 个样本,即 256 * 3 个浮点数.

image_1793622965314_0.png

image_1793623526466_0.png

输出多份 CSV,慢转 S 共 4 份数据集,快转 F 共 4 份数据集,停转 X 共 5 份.

image_1793623678389_0.png

image_1793623628394_0.png

环境配置

我们需要安装 NanoEdge AI Studio,由于笔者开发环境在 Linux 上,安装方法略有不同,先前往 https://stm32ai.st.com/download-nanoedgeai/ 填写信息拿到安装包.

image_1793565699319_0.png

下载安装包后,使用 apt install 命令即可安装完成.

image_1793565962658_0.png

训练模型

打开 NEAI Studio 后,创建项目,选择 Classify 因为我们要分类不同的状态.

image_1793268335149_0.png

选择 NUCLEO-H533RE 这块板子,这里其实影响不大,因为我们不用 NEAI Studio 做数据采集.

image_1793268410800_0.png

传感器一栏选择「时序数据」的加速度计,3 轴.

image_1793268899842_0.png

之后我们从处理过的数据中导入 CSV 就可以了.

image_1793281034687_0.png

在左侧导入数据后,右侧自动出现了不同分类的数据图表.

image_1793433246054_0.png

然后开始数据训练,训练完成后准确度会达到比较高的值.

image_1793433319397_0.png

image_1793626387692_0.png

然后我们可以查看遍历的不同组合的模型的具体性能指标,包含准确度、RAM/Flash 占用、执行时间等,我们就选用综合排序最高的这个模型.

image_1793457606095_0.png

部署模型时,我们打开浮点 ABI,不过这也要求我们的 Zephyr 程序也要打开浮点 ABI.

image_1793457655050_0.png

拿到编译产物后,就可以开始着手接入我们的应用了.

image_1793626834579_0.png

我们首先在 CMakeLists 中,加入 include 目录,并且把 libneai.a 加入链接库列表.

image_1793626716461_0.png

然后我们在 Zephyr 编写一个这样的推理函数即可.

image_1793626650191_0.png

然后 OLED 上面将分类结果绘制出来即可.

image_1793627040468_0.png

为了更好可视化数据,我们还实现了一套 FFT 频谱计算,并显示在 OLED 上.

image_1793627406384_0.png

扩大 RAM 布局

接入模型后,我们开头试跑的几次采样是可以输出 tag 的,但是之后就会发生 bus fault,使用 addr2line 工具考证后发现程序跑飞,怀疑 stack 大小不足导致 stack 本身的数据被破坏.

image_1793465350105_0.png

解决方法也很简单,直接在 Zephyr 的 prj.conf 中,设置合理的 stack 大小,同时考虑到可能会有潜在的 heap 上的分配,我们一并扩大.

CONFIG_MAIN_STACK_SIZE=8192
CONFIG_HEAP_MEM_POOL_SIZE=16384

最终效果

我们用一个小键盘接到电脑上,焦点对准串口的窗口,这样就可以按键控制电机状态,按键 1 进入慢转,按键 2 进入快转,按键 3 停转,可以看到屏幕上显示了实时频谱和 NEAI 库的推理结果,可以看到是一致的.

classify.gif



共1条 1/1 1 跳转至页

回复

匿名不能发帖!请先 [ 登陆 注册 ]