- 2026-04-25
-
发表了主题帖:
【STM32MP257F-DK测评】speexdsp和webrtc 的NS算法比较
在上一篇文章中,使用speexdsp的库文件来实现单通道音频的降噪测试。总体效果还挺好。另外在以前工作中也有使用到webrtc项目中的降噪处理。2种降噪的处理方式有所不同。
我也可以将它移植到stm32mp2上。用同样的方式测试。
即 stm32mp2枚举成usb声卡,电脑同时播放音乐和录音。在stm32mp2中,用speexdsp的方式处理左声道,用webrtc ns的方式来处理右声道。这样电脑的录音文件刚好包含2个声道为不同处理结果的音频。
一、移植webrtc ns算法
首先,因为stm32mp2 sdk中并没有现成的webrtc ns库。需要自己重新下载源代码和编译文件。
源代码可下载 https://github.com/ROAD2018/AEC-ANS-AGC/blob/master/NS/ 中的文件 noise_suppression.c/noise_suppression.h。
在webrtc的原项目中noise_suppression由多个文件组成,移植比较麻烦。而这git仓库的文件已经将多个文件合并成1个源文件。方便我们直接使用和测试。
webrtc ns的使用,实际就是调用noise_suppression.h中的API函数。大致流程为:
1. 创建WebRtcNs 降噪器(右声道)
NsHandle *ns_handle_right;
ns_handle_right = WebRtcNs_Create();
2. 初始化WebRtcNs 降噪器,实际就是设置采样率(16K)
WebRtcNs_Init(ns_handle_right, NS_SAMPLE_RATE) ;
3. 设置降噪等级(0-3,3为最大降噪)
WebRtcNs_set_policy(ns_handle_right, 3);
4. 分析输入音频的噪声(循环执行)
WebRtcNs_Analyze(ns_handle_right, right_buffer_16k);
5. 处理音频数据,应用降噪(循环执行)
WebRtcNs_Process(ns_handle_right, webrtns_buffer, 1, webrtns_buffer);
6. 最后,在处理结束后,将WebRtcNs 降噪器做资源释放
WebRtcNs_Free(ns_handle_right);
其中,WebRtcNs的的采样率只能设置为16K或者8K。由于声卡的采样率为48000。在我测试中,是直接将48000音频进行3分之1抽样后给到webrtcNs处理,处理完再进行3倍插值处理。
代码可以参照:
三、降噪处理效果对比
处理后的结果文件如下:(左声道为speexdsp结果,右声道为webrtcns结果,和原文件对比,可以看出2个结果都有降噪的效果)
原始音频文件:
以下是我对2个处理结果的个人的看法:
1. webrtc的采样率为16K,speexdsp的采样率为48K。speexdsp的语音还原度更高。
2. webrtc的最大降噪效果大概为24db,speexdsp的降噪效果可达到40db。单从噪声消除的降低效果上speexdsp更好。
3. webrtc的处理数据块是固定,为160个采样点处理一次。speexdsp的处理数据块是可调的,相对而言,延时更短。
4. webrtc的处理时间更短,暂用算力更低。
5. speexdsp存在以下问题,当完全没有输入信号后,speexdsp处理会重新收敛。此时重新有输入信号,会出现噪声泄露的现象。如下图:
四、算力评估
以上的代码同时运行了speexdsp ns 和 webrtc ns的处理。这个时候,可以同时测试一下stm32mp2上的实际算力如何。
可以在程序执行中,在另一个终端来执行top命令,查看程序占用的cpu百分比。
以下为我的测试结果:
1. 在程序使用 -g 选项编译,即不使用优化等级编译。程序执行占用的cpu百分比为10.6%。
1. 在程序使用 -O2 选项编译,即使用较优的优化等级编译。程序执行占用的cpu百分比为6.6%。
这样的算力消耗比较理想。stm32mp2的算力还是很给力的。
以上便是我对speexdsp 和 webrtc 降噪处理的学习内容。
- 2026-04-24
-
回复了主题帖:
【STM32MP257F-DK测评】speexdsp的NS算法移植和测试
Jacktang 发表于 2026-4-18 09:57
speexdsp对平缓噪声的抑制效果不错
那如何判断一段音频是否包含人声有什么思路?
这得用ai人声识别
- 2026-04-14
-
发表了主题帖:
【STM32MP257F-DK测评】speexdsp的NS算法移植和测试
本帖最后由 latera 于 2026-4-14 22:53 编辑
一、speexdsp库说明
SpeexDSP 是一个开源的音频信号处理库,专注于为语音通信提供实时处理功能。它是经典语音编解码器项目 Speex 的一部分,但独立于编解码器,专门处理原始音频信号。
它的核心功能包括:
回声消除 (AEC):消除在语音通话中,自己扬声器播放的声音被麦克风再次采集产生的回声。
噪声抑制 (NS):降低背景环境噪声,提升语音清晰度。
自动增益控制 (AGC):自动调整录音音量,使声音大小保持稳定。
静音检测 (VAD):判断一段音频是否包含人声,常用于节省带宽或触发语音识别。
二、speexdsp库使用
STM32MP2的文件系统默认包含speexdsp的库文件和头文件。
在程序中,包含头文件,并按初始化设置 --> 设置参数 --> 周期处理 --> 最后销毁 的步骤来编写程序便可。如我要使用噪声抑制的方法:
1. 初始化预处理器(噪声抑制)
SpeexPreprocessState *preprocess_state;
preprocess_state = speex_preprocess_state_init(frame_size, sample_rate); // frame_size 表示一帧音频数据的长度
2. 配置噪声抑制参数和噪声抑制强度
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_DENOISE, &denoise_enabled); // denoise_enabled = 1 (1表示使能)
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_NOISE_SUPPRESS, &noise_suppress_level); // noise_suppress_level = -25 (单位db,)
3. 应用噪声抑制
speex_preprocess(preprocess_state, samples, NULL); // samples 为一帧单声道的数据。
程序参考(在后文有测试说明):
#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <speex/speex_preprocess.h>
#include <string.h>
#define DEVICE "hw:1,0"
#define SAMPLE_RATE 48000
#define CHANNELS 2
#define SAMPLE_SIZE 2 /* 16-bit */
#define PERIOD_SIZE 256
#define BUFFER_SIZE (8 * PERIOD_SIZE)
#define RING_BUFFER_SIZE 16 /* 环形缓冲区大小(周期数) */
// 环形缓冲区结构
typedef struct {
char *data;
int size;
int period_size;
int write_pos;
int read_pos;
sem_t empty;
sem_t full;
pthread_mutex_t mutex;
} RingBuffer;
// 全局变量
snd_pcm_t *capture_handle, *playback_handle;
RingBuffer ring_buffer;
SpeexPreprocessState *preprocess_state;
// 初始化环形缓冲区
int init_ring_buffer(RingBuffer *rb, int period_size, int num_periods) {
rb->period_size = period_size * CHANNELS * SAMPLE_SIZE;
rb->size = num_periods;
rb->data = (char *)malloc(rb->period_size * num_periods);
if (!rb->data) {
fprintf(stderr, "无法分配环形缓冲区\n");
return -1;
}
rb->write_pos = 0;
rb->read_pos = 0;
sem_init(&rb->empty, 0, num_periods);
sem_init(&rb->full, 0, 0);
pthread_mutex_init(&rb->mutex, NULL);
return 0;
}
// 写入环形缓冲区
void write_ring_buffer(RingBuffer *rb, char *data) {
sem_wait(&rb->empty);
pthread_mutex_lock(&rb->mutex);
memcpy(rb->data + rb->write_pos * rb->period_size, data, rb->period_size);
rb->write_pos = (rb->write_pos + 1) % rb->size;
pthread_mutex_unlock(&rb->mutex);
sem_post(&rb->full);
}
// 从环形缓冲区读取
void read_ring_buffer(RingBuffer *rb, char *data) {
sem_wait(&rb->full);
pthread_mutex_lock(&rb->mutex);
memcpy(data, rb->data + rb->read_pos * rb->period_size, rb->period_size);
rb->read_pos = (rb->read_pos + 1) % rb->size;
pthread_mutex_unlock(&rb->mutex);
sem_post(&rb->empty);
}
// 清理环形缓冲区
void cleanup_ring_buffer(RingBuffer *rb) {
free(rb->data);
sem_destroy(&rb->empty);
sem_destroy(&rb->full);
pthread_mutex_destroy(&rb->mutex);
}
// 采集线程函数
void *capture_thread(void *arg) {
short *buffer = (short *)malloc(PERIOD_SIZE * CHANNELS * SAMPLE_SIZE);
if (!buffer) {
fprintf(stderr, "无法分配采集缓冲区\n");
return NULL;
}
short *samples = (short *)malloc(PERIOD_SIZE*SAMPLE_SIZE);
int err;
while (1) {
// 从捕获设备读取数据
if ((err = snd_pcm_readi(capture_handle, buffer, PERIOD_SIZE)) != PERIOD_SIZE) {
if (err == -EPIPE) {
fprintf(stderr, "捕获设备发生溢出,正在恢复...\n");
snd_pcm_prepare(capture_handle);
continue;
} else {
fprintf(stderr, "读取捕获设备失败: %s\n", snd_strerror(err));
break;
}
}
// 使用 speexdsp 进行降噪处理
for(int i=0;i<PERIOD_SIZE;i++){
samples[i] = buffer[CHANNELS*i];
}
speex_preprocess(preprocess_state, samples, NULL);
for(int i=0;i<PERIOD_SIZE;i++){
buffer[CHANNELS*i] = samples[i] ;
}
// 写入环形缓冲区
write_ring_buffer(&ring_buffer, buffer);
}
free(buffer);
return NULL;
}
// 播放线程函数
void *playback_thread(void *arg) {
char *buffer = (char *)malloc(PERIOD_SIZE * CHANNELS * SAMPLE_SIZE);
if (!buffer) {
fprintf(stderr, "无法分配播放缓冲区\n");
return NULL;
}
int err;
while (1) {
// 从环形缓冲区读取数据
read_ring_buffer(&ring_buffer, buffer);
// 写入播放设备
if ((err = snd_pcm_writei(playback_handle, buffer, PERIOD_SIZE)) != PERIOD_SIZE) {
if (err == -EPIPE) {
fprintf(stderr, "播放设备发生下溢,正在恢复...\n");
snd_pcm_prepare(playback_handle);
continue;
} else {
fprintf(stderr, "写入播放设备失败: %s\n", snd_strerror(err));
break;
}
}
}
free(buffer);
return NULL;
}
int main() {
int err;
snd_pcm_hw_params_t *hw_params;
pthread_t capture_tid, playback_tid;
// 初始化环形缓冲区
if (init_ring_buffer(&ring_buffer, PERIOD_SIZE, RING_BUFFER_SIZE) < 0) {
return EXIT_FAILURE;
}
// 打开捕获设备
if ((err = snd_pcm_open(&capture_handle, DEVICE, SND_PCM_STREAM_CAPTURE, 0)) < 0) {
fprintf(stderr, "无法打开捕获设备: %s\n", snd_strerror(err));
cleanup_ring_buffer(&ring_buffer);
return EXIT_FAILURE;
}
// 打开播放设备
if ((err = snd_pcm_open(&playback_handle, DEVICE, SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
fprintf(stderr, "无法打开播放设备: %s\n", snd_strerror(err));
snd_pcm_close(capture_handle);
cleanup_ring_buffer(&ring_buffer);
return EXIT_FAILURE;
}
// 配置捕获设备
if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0) {
fprintf(stderr, "无法分配硬件参数结构: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_any(capture_handle, hw_params)) < 0) {
fprintf(stderr, "无法初始化硬件参数: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
fprintf(stderr, "无法设置访问类型: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_format(capture_handle, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) {
fprintf(stderr, "无法设置格式: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_rate(capture_handle, hw_params, SAMPLE_RATE, 0)) < 0) {
fprintf(stderr, "无法设置采样率: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_channels(capture_handle, hw_params, CHANNELS)) < 0) {
fprintf(stderr, "无法设置通道数: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_period_size(capture_handle, hw_params, PERIOD_SIZE, 0)) < 0) {
fprintf(stderr, "无法设置周期大小: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_buffer_size(capture_handle, hw_params, BUFFER_SIZE)) < 0) {
fprintf(stderr, "无法设置缓冲区大小: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0) {
fprintf(stderr, "无法设置捕获设备参数: %s\n", snd_strerror(err));
goto cleanup;
}
// 配置播放设备
if ((err = snd_pcm_hw_params_any(playback_handle, hw_params)) < 0) {
fprintf(stderr, "无法初始化播放设备硬件参数: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_access(playback_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
fprintf(stderr, "无法设置播放设备访问类型: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_format(playback_handle, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) {
fprintf(stderr, "无法设置播放设备格式: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_rate(playback_handle, hw_params, SAMPLE_RATE, 0)) < 0) {
fprintf(stderr, "无法设置播放设备采样率: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_channels(playback_handle, hw_params, CHANNELS)) < 0) {
fprintf(stderr, "无法设置播放设备通道数: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_period_size(playback_handle, hw_params, PERIOD_SIZE, 0)) < 0) {
fprintf(stderr, "无法设置播放设备周期大小: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_buffer_size(playback_handle, hw_params, BUFFER_SIZE)) < 0) {
fprintf(stderr, "无法设置播放设备缓冲区大小: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params(playback_handle, hw_params)) < 0) {
fprintf(stderr, "无法设置播放设备参数: %s\n", snd_strerror(err));
goto cleanup;
}
snd_pcm_hw_params_free(hw_params);
// 初始化 speex 降噪器
preprocess_state = speex_preprocess_state_init(PERIOD_SIZE, SAMPLE_RATE);
if (!preprocess_state) {
fprintf(stderr, "无法初始化 speex 降噪器\n");
goto cleanup;
}
// 设置降噪参数
int denoise_enabled = 1;
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_DENOISE, &denoise_enabled);
int noise_suppression = -40;
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_NOISE_SUPPRESS, &noise_suppression);
// 启动播放设备(填充初始缓冲区)
char *init_buffer = (char *)calloc(PERIOD_SIZE * CHANNELS * SAMPLE_SIZE, 1);
if (init_buffer) {
for (int i = 0; i < 3; i++) {
snd_pcm_writei(playback_handle, init_buffer, PERIOD_SIZE);
}
free(init_buffer);
}
// 创建线程
if (pthread_create(&capture_tid, NULL, capture_thread, NULL) != 0) {
fprintf(stderr, "无法创建采集线程\n");
goto cleanup;
}
if (pthread_create(&playback_tid, NULL, playback_thread, NULL) != 0) {
fprintf(stderr, "无法创建播放线程\n");
goto cleanup;
}
// 主循环
printf("开始音频环回测试...\n");
printf("按Ctrl+C退出\n");
while (1) {
sleep(1);
}
cleanup:
// 清理资源
if (preprocess_state) {
speex_preprocess_state_destroy(preprocess_state);
}
if (hw_params) {
snd_pcm_hw_params_free(hw_params);
}
snd_pcm_close(capture_handle);
snd_pcm_close(playback_handle);
cleanup_ring_buffer(&ring_buffer);
return EXIT_FAILURE;
}
Makefile文件为
# Makefile for ALSA loopback test
# Ensure cross-compilation environment is set up
ifeq ($(CC),)
$(warning Cross-compilation environment not set up!)
$(warning Please run: source ../../SDK/environment-setup-cortexa35-ostl-linux)
$(warning before building this project.)
endif
CFLAGS = -Wall -Wextra -g
LDFLAGS = -lasound -lspeexdsp -pthread
TARGET = loopback
SOURCES = loopback.c
all: $(TARGET)
$(TARGET): $(SOURCES)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
clean:
rm -f $(TARGET)
help:
@echo "ALSA Loopback Test Makefile"
@echo "============================"
@echo "make - 编译程序"
@echo "make clean - 清理编译产物"
@echo "make help - 显示此帮助信息"
.PHONY: all clean help
三、speexdsp NS 测试
我同样使用 usb声卡来测试处理效果,声卡修改参考:【STM32MP257F-DK测评】usb声卡修改 https://bbs.eeworld.com.cn/thread-1345436-1-1.html
在上面的程序中,我将uac声卡设置成16bit的双声道音频。uac同时播放和录音。其中,读取uac声卡数据后,取出左声道来作为speexdsp ans 处理信号,右声道直接不做处理。最后将这2声道的数据从声卡播放出去。电脑的录音,就能获得处理前后的效果对比文件。
测试音频为一段歌曲和白噪声的混音音频。测试结果如下:
可以看出speexdsp对平缓噪声的抑制效果还是比较好的。测试音频见附件:
- 2026-04-12
-
回复了主题帖:
【STM32MP257F-DK测评】usb声卡修改
Jacktang 发表于 2026-4-12 09:25
开始要将usb设备枚举成一个虚拟网卡,是为了后面进行USB Gadget USB好驱动么
原厂镜像是已经枚举成一个虚拟网卡,我只是想分析它是怎么做到,好仿照它原脚本的做法。
- 2026-04-01
-
发表了主题帖:
【STM32MP257F-DK测评】usb声卡修改
本帖最后由 latera 于 2026-4-1 22:09 编辑
前面文章提到,st的原厂镜像启动后,usb设备为一个usb ncm虚拟网卡设备。
可以理解为开发板在系统启动时,会执行一个程序或脚本,将usb设备枚举成一个虚拟网卡。执行这个程序或脚本的前提是内核或者驱动支持这类usb设备的创建和运行。所以,我以下修改的内容,实际就是让内核支持uac设备,并且写一个启动脚本,让usb枚举成一个uac设备。
一、内核编译与uac编译选项修改
内核编译的步骤可以参考:https://wiki.stmicroelectronics.cn/stm32mpu/wiki/Getting_started/STM32MP2_boards/STM32MP257x-DK/Develop_on_Arm_Cortex-A35/Modify,_rebuild_and_reload_the_Linux_kernel
大致步骤为:
1. 下载解压 开发包源码 https://www.st.com/en/embedded-software/stm32mp2dev.html#get-software (MP2-DEV-SRC),里面包含kernel的源代码
2. source SDK/environment-setup-cortexa35-ostl-linux,配置交叉工具链的环境变量,方便接下来编译kernel时,系统使用正确的gcc路径。
3. make O="${OUTPUT_BUILD_DIR}" defconfig fragment*.config,配置内核。文件夹其中的多个.config文件和defconfig文件将最终组成内核编译的配置文件。
4. make Image.gz vmlinux dtbs O="${OUTPUT_BUILD_DIR}",编译内核镜像
5. make modules O="${OUTPUT_BUILD_DIR}",编译内核模块。
6. 将编译的内核镜像和内核模块.ko文件,传输到开发板系统中的/boot。就完成了系统的替换。
一、usb ncm的实现分析
首先需要了解USB Gadget USB驱动的原理。
USB 设备驱动按照设备端关联的 USB 控制器是工作在主模式还是从模式,分为 USB 设备主机侧驱动 (主模式驱动),或者 USB 设备从机侧驱动 (从模式驱动)。
Linux 下将 USB 设备从机侧驱动称为 USB Gadget 驱动。USB Gadget 驱动是通过USB 来模拟其它类型的设备,如USB Gadget UAC 驱动 用来模拟声卡外设;USB Gadget Serial 驱动用来模拟串口外设等等。
USB Gadget 驱动,包括USB设备控制器(UDC) 驱动 和 Gadget功能(function)驱动,两大部分。
其中,USB设备控制器(UDC) 驱动,负责USB 设备控制器(UDC) 和主机侧 USB 控制器(UHC) 之间的数据传输。
而 Gadget 功能驱动(function) 负责实现功能协议(如 UDC 等)实现。USB 设备控制器(UDC) 驱动和Gadget 功能驱动(function) 彼此之间也会进行数据交互。
下面是查看开发板系统,推出usb ncm是怎么配置出来的。
1. 通过udev热插拔机制,调用/etc/udev/rules.d/97-usbotg.rules规则。
97-usbotg.rules 中实际调用的是/sbin/stm32_usbotg_eth_via_udev.sh
ACTION=="add", SUBSYSTEM=="udc", RUN+="/sbin/stm32_usbotg_eth_via_udev.sh start"
ACTION=="remove", SUBSYSTEM=="udc", RUN+="/sbin/stm32_usbotg_eth_via_udev.sh stop"
2. /sbin/stm32_usbotg_eth_via_udev.sh 实际是调用 /sbin/stm32_usbotg_eth_config.sh
3. stm32_usbotg_eth_config.sh中的作用大概为: 在 /sys/kernel/config/usb_gadget 文件夹中生成相应usb配置文件。来告诉usb gadget框架需要配置成什么类型的usb设备。
二、修改内核支持uac
为了实现uac功能,首先需要内核支持uac设备。使用make menuconfig修改usb gadget的配置。
需要将USB_CONFIGFS_F_UAC1 和 USB_CONFIGFS_F_UAC2选上
因为 USB_GADGET 默认选择为编译成内核模块,那依赖它的USB_CONFIGFS_F_UAC1 和 USB_CONFIGFS_F_UAC2也将作为内核模块。这样的话,需要重新编译内核模块,而不是重新编译内核。
make modules编译后,新增出新的文件为:
u_audio.ko, usb_f_uac1.ko, usb_f_uac2.ko
将这3个文件传输到开发板的内核模块文件夹中。并命令行执行 /sbin/depmod -a。刷新内核模块的依赖关系。
三、编写启动文件
配置文件stm32_usbotg_uac_config.sh如下:
大致内容:
1. 配置usb的vid,pid
2. 在config文件夹设置usb的描述符和供电等配置
3. 在function文件夹中新增uac文件夹,usb gadget将自动产生uac相应的配置文件
4. 设置UDC,以启动USB,如下:
# usb_gadget依赖于libcomposite模块
modprobe libcomposite
# 挂载config文件系统
mount -t configfs none /sys/kernel/config
# 创建g1目录,实例化一个新的gadget模板
echo "mkdir /sys/kernel/config/usb_gadget/g1"
mkdir -m 0770 /sys/kernel/config/usb_gadget/g1
# 设置产品的VendorID、ProductID及USB规范版本号
echo "Setting Vendor Product ID's and bcdDevice"
echo 0x0032 > /sys/kernel/config/usb_gadget/g1/idVendor
echo 0x0019 > /sys/kernel/config/usb_gadget/g1/idProduct
# 设备版本号
echo 0x0200 > /sys/kernel/config/usb_gadget/g1/bcdDevice
# USB 1.1: 0x0110
# USB 2.0: 0x0200, USB 2.1: 0x0210, USB 2.5: 0x0250
# USB 3.0: 0x0300, USB 3.1: 0x0310, USB 3.2: 0x0320
# echo 0x0210 > /sys/kernel/config/usb_gadget/g1/bcdUSB
# 实例化英语ID,开发商、产品和序列号字符串写入内核
echo "Setting English strings"
mkdir -m 0770 /sys/kernel/config/usb_gadget/g1/strings/0x409
echo "0123456789ABCDEF" > /sys/kernel/config/usb_gadget/g1/strings/0x409/serialnumber
echo "rockchip" > /sys/kernel/config/usb_gadget/g1/strings/0x409/manufacturer
echo "USB Audio Device" > /sys/kernel/config/usb_gadget/g1/strings/0x409/product
# Creating Config
echo "Creating Config"
mkdir -m 0770 /sys/kernel/config/usb_gadget/g1/configs/c.1
mkdir -m 0770 /sys/kernel/config/usb_gadget/g1/configs/c.1/strings/0x409
echo "uac2" > /sys/kernel/config/usb_gadget/g1/configs/c.1/strings/0x409/configuration
echo 500 > /sys/kernel/config/usb_gadget/g1/configs/c.1/MaxPower
# bind functions
# uac2 need to put before uvc, otherwise uvc + uac2 enumerate failed in win10
echo "Creating UAC1 gadget functionality : uac1.0"
mkdir /sys/kernel/config/usb_gadget/g1/functions/uac1.0
echo 48000 > /sys/kernel/config/usb_gadget/g1/functions/uac1.0/c_srate # 捕获采样率
echo 48000 > /sys/kernel/config/usb_gadget/g1/functions/uac1.0/p_srate # 播放采样率
ln -s /sys/kernel/config/usb_gadget/g1/functions/uac1.0 /sys/kernel/config/usb_gadget/g1/configs/c.1
# Binding USB Device Controller
echo "Binding USB Device Controller"
echo 48300000.usb > /sys/kernel/config/usb_gadget/g1/UDC
三、测试声卡播放和录音
在重启usb前先关闭usb ncm的配置,执行 /sbin/stm32_usbotg_eth_config.sh stop
然后执行stm32_usbotg_uac_config.sh start,并连接usb到电脑,电脑将显示新的录音设备和播放设备。
同时在开发板系统中执行aplay -l,将查看到系统新增了一个card 1的uac设备。
1.使用apaly recorder测试
在电脑播放音乐(选择新增的声卡作为播放设备),用录音软件来同时录制声卡的声音。
开发板上执行arecord -D hw:1,0 -c 2 -r 48000 -f S16_LE -v | aplay -D hw:1,0 -f S16_LE -c 2 -r 48000 -v。其中hw:1,0为uac声卡。
将录音保存下来,可以重新播放出来,如果声音正常,便可以判断uac能正常播放和录音。
2.使用alsa库测试
同样,可以用alsa库在c代码上实现一个loopback 的环回测试程序,代码见附件:
大致程序为使用snd_pcm_readi 读取声卡数据(电脑播放), snd_pcm_writei 写入声卡数据(电脑播放)。
while (1) {
// 从捕获设备读取数据
if ((err = snd_pcm_readi(capture_handle, buffer, PERIOD_SIZE)) != PERIOD_SIZE) {
if (err == -EPIPE) {
fprintf(stderr, "捕获设备发生溢出,正在恢复...\n");
snd_pcm_prepare(capture_handle);
continue;
} else {
// fprintf(stderr, "读取捕获设备失败: %s\n", snd_strerror(err));
// goto cleanup;
}
}
// 写入播放设备
if ((err = snd_pcm_writei(playback_handle, buffer, PERIOD_SIZE)) != PERIOD_SIZE) {
if (err == -EPIPE) {
fprintf(stderr, "播放设备发生下溢,正在恢复...\n");
snd_pcm_prepare(playback_handle);
continue;
} else {
// fprintf(stderr, "写入播放设备失败: %s\n", snd_strerror(err));
// goto cleanup;
}
}
}
测试现象如下:
这样就完成uac的测试了。上位机实现的是uac1.0,也可以修改stm32_usbotg_uac_config.sh改为uac2.0来测试,因为默认依然为2通道声卡,所有测试测试效果是一样的。
- 2026-03-29
-
发表了主题帖:
【STM32MP257F-DK测评】SDK开发环境搭建及测试实时性
本帖最后由 latera 于 2026-3-29 23:58 编辑
一、搭建环境
前面文章提到ST为MPU开发定义了3种开发包,对应3种开发阶段。之前说的starter package 是原厂标配的烧录镜像。第2种开发包是SDK + 内核 uboot等源码。SDK里包含编译应用 和 内核的交叉工具链,也包括编译时用的头文件和库文件。
其实,可以在开发板系统中,通过apt-get install 安装各种工具和软件。但是实践下来并不能轻易下载到。主要是要开发板能连接到外网(获取 http://packages.openstlinux.st.com/6.2 软件包)。可以参考Package repository for OpenSTLinux distribution - stm32mpu,来试试能不能下载。因为我现在并不需要下载大量的软件和工具。并且我想测试一下交叉编译的工具链。
安装SDK的官方参考教程:Install the SDK - stm32mpu,其中:
SDK下载:https://www.st.com/en/embedded-software/stm32mp2dev.html#get-software 中MP2-DEV-x86,我是在x86电脑的虚拟机安装ubuntu22来开发程序的。
源码下载:https://www.st.com/en/embedded-software/stm32mp2dev.html#get-software 中的MP2-DEV-SRC。
二、测试hello world
SDK解压后,并根据官方教程执行一个脚本,交叉工具链便安装成功。并不需要重新编译生成。同时文件夹中包含一个配置文件 environment-setup-cortexa35-ostl-linux
这个文件中指定gcc等工具的文件名和路径,也指定了头文件和库文件的路径。在编译应用程序前可以先source一下这个文件。
例如:
新建一个hello_world.c 文件
#include "stdio.h"
int main(int argc,
char **argv)
{
printf("Hello World\n");
return 0;
}
并新建一个Makefile。
PROG = hello_world
SRCS = hello_world.c
CLEANFILES = $(PROG)
all: $(PROG)
$(PROG): $(SRCS)
$(CC) -o $@ $^ $(CFLAGS) $(LDFLAGS)
clean:
rm -f $(CLEANFILES) $(patsubst %.c,%.o, $(SRCS))
执行 source xxxx/SDK/environment-setup-cortexa35-ostl-linux
再执行 make 将得到一个hello_world 的文件,放到开发板上,执行将得到如下结果
这样便表示gcc的编译是完全正确的。
三、交叉编译rt-test
为了测试STM32MP2的实时性,需要 用到2个软件工具,cyclictest + stress-ng。
cyclictest是用于测试中断延时加调度延时,中断延时(interrupt latency),即中断发生到进入中断处理程序ISR的延时。调度延时(scheduling latency),即当任务被唤醒到任务真正获得CPU使用权中间的延时。
stress-ng是系统压力测试工具。如
CPU(各种指令集、加密、浮点、乱序执行等)
内存(分配、页面错误、缓存跳跃、碎片化等)
磁盘(顺序/随机读写、fsync、fallocate、mmap 等)
I/O(hdd、aio、ioprio、sync 等)
网络、调度、虚拟内存、管道、零设备等几乎所有子系统
实际作用是为了在cyclictest 测试时,让内存,磁盘,cpu进行各种工作,增多系统的工作内容,增大系统调度延时的可能性。这样能更准确地获取整个系统实时性能。
cyclictest的源码仓库为https://git.kernel.org/pub/scm/utils/rt-tests/rt-tests.git
我选择下载的是rt-tests-2.2.tar.gz
下载后解压,需要修改Makefile。
1. 注释掉CC 和 AR定义,在make之前通过 source environment-setup-cortexa35-ostl-linux来指定。
2. 修改make install 的文件夹路径为当前文件夹下
这样执行 make 和 make install,便能在当前文件夹的usr/local/bin 得到一个cyclictest的执行文件,将它传输到开发板。
四、交叉编译stress-ng
stress-ng 的源码仓库在https://github.com/ColinIanKing/stress-ng/releases ,我选择的是stress-ng-0.13.01.tar.gz。
下载并解压,因为Makefile并没有指定CC,这样反而简单,执行source environment-setup-cortexa35-ostl-linux就可以,再将make install的路径改为本文件夹内即可。
make 、make install后,在本文件夹内就可以得到stree-ng的可执行文件,同样将它传输到开发板中。
四、cpu实时性能测试
首先将cpu的主频提到最高:
root@stm32mp2-e3-d6-87:/# echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
查看cpu主频为1.5GHZ
1. 空载测试
终端执行:./cyclictest -m -Sp99 -i1000 -h800 -D10m
其中各个命令参数表示
-m:锁定当前和未来的内存分配,防止测试进程被交换到磁盘,避免换页操作引入额外延迟。
-Sp99:将测试线程的调度策略设置为 SCHED_FIFO(实时先进先出策略),并将其优先级设置为 99(这是Linux中用户空间可设置的最高实时优先级)。
-i1000:指定测试的基本间隔为 1000微秒(1毫秒)。测试线程将尝试每隔1毫秒唤醒一次并计算实际延迟。
-h800:输出一个大小为 800 个桶的直方图,用于统计不同延迟区间的发生次数,更细致地展示延迟分布。
-D10m:指定测试总运行时长为 10分钟。
运行结果如下:平均延时9us,但是最大延时有1646us。
2. 满载测试
stress-ng -c 2 --io 2 --vm 2 --vm-bytes 32M --timeout 100s &
其中命令为:
-c 2:启动 2 个工作者进程,专门执行 CPU 压力测试(例如计算素数)。
--io 2:启动 2 个工作者进程,专门执行 I/O 压力测试(例如频繁的文件读写)。
--vm 2:启动 2 个工作者进程,专门执行内存压力测试。
--vm-bytes 32M:指定每个内存压力测试进程使用 32MB 的内存块进行操作。
--timeout 100s:指定整个压力测试持续运行 100 秒后自动停止
测试结果如下,平均延时22us,但是最大延时有1717us。
之前使用rk3308(4核A35@1.3GHZ)进行同样测试,空载的平均延时为20us,最大延时为1400us。满载的平均延时为30us,最大延时为2000us。
可看出STM32MP2在高主频的加持下,平均系统延时更短,不过因为标准linux内核并不是实时内核,存在抖动,造成系统的最大延时较大,但是1ms多的延时基本满足实时要求。
后续也可以尝试打上实时补丁,试试效果。
另外这里测试的时长是为10分钟,并不十分严谨,实际测试需要数小时或1天多。
-
回复了主题帖:
【STM32MP257F-DK测评】性能测试
秦天qintian0303 发表于 2026-3-29 19:02
有没有那种比较符合标准的测试工具?
memtester 算是内存压测的标配
- 2026-03-28
-
发表了主题帖:
【STM32MP257F-DK测评】性能测试
本帖最后由 latera 于 2026-3-28 18:02 编辑
上一篇只是将开发板跑起来。并没有详细说开发板如何使用和开发。不过在开发之前可以先了解一下开发板的配置和性能。毕竟stm32mp2芯片是st近年来开发的最强mpu。得看看这个开发板的用料是否能配得上这个mpu性能。
一、开发板性能
下表为开发板的主要芯片的型号和配置:
芯片型号
芯片类型
型号/规格
关键性能与说明
STM32MP257FA
MPU主控(SoC)
双核 Arm Cortex-A35
+ Cortex-M33 + M0+
A35最高1.5GHz + M33 400MHz (STMicroelectronics)
支持Linux + 实时控制(异构架构)
内置GPU(OpenGL/Vulkan)、NPU- 支持DDR3/DDR4/LPDDR4(最大4GB)
丰富接口:Ethernet、USB3.0、PCIe、MIPI等
MT53E1G32D2FW-046
DDR 运行内存
LPDDR4
1Gbit x32(4Gbytes)
Micron DDR4
数据率约4266MT/s
用于系统主内存
高带宽支持Linux系统
THGBMUG6C1LBAIL
eMMC存储
eMMC 5.1 NAND Flash 8GB
Kioxia(原东芝)eMMC
容量常见 8GB/16GB/32GB
支持HS400高速模式
用于系统启动+文件系统
LBEE5KL1YN-81
无线通信模块
IEEE802.11b/g/n W-LAN
Bluetooth® v5.2 (BR/EDR/BLE)
Murata模块
SDIO (WLAN)
UART (BT)
STLINK_V3EC
调试/下载器
ST官方调试接口模块
支持SWD/JTAG调试
RTL8211F-CG
以太网PHY
千兆以太网PHY
支持10/100/1000 Mbps
RGMII接口
低功耗设计
常用于工业/嵌入式网络
其中stlink应该是用调试stm32mp2中的M33核。ddr和emmc的容量和速度都这些年开发板的高配了。开发各种项目完全没有问题。
二、连接方式
开发之前,首先要了解怎么连接到开发板,方便调试和传输文件。之前开发一般会使用串口调试内核启动和打印信息,usb adb 和网络来传输文件和调试应用。stm32mp257f-dk的文件系统没有并没有adb,可能要重新构建yocto的文件系统或者自己编译一个才行。所以,下面介绍系统已经有的连接开发板的方式。
1. 串口终端连接
上面提到板载的stlink有串口连接到stm32mp2主芯片上,并且stlink在电脑上枚举出了串口设备。可以通过串口终端软件连接到开发板。
stlink的驱动在在安装stm32CubeProg软件时已经自动安装了。需要注意的是,stlink连接电脑时,会出现2个串口,选择串口号大的一个。
我是MobaXterm软件,在串口配置上选择如下。波特率为115200
串口波特率可以在系统的启动打印信息上找到对应的。登录时不需要密码
2. usb 虚拟网络连接
通过ifconfig,可以看到usb虚拟网口(usb0)的IP为192.168..7.1
同样,可以通过MobaXterm软件,ssh连接到开发板的终端。也可以通过SFTP或者SCP发送和下载文件。用户名为root,没有密码。
3. 网络连接
网口也是可以连接的,不过开发板系统默认是dhcp, 直连电脑时,要先在开发板上设置好IP,如下,用ifconfig设置网口end0的IP为169.25.1.100。即可用MobaXterm的ssh连接开发板
4.wifi连接热点
无线连接可以参考How to setup a WLAN connection - stm32mpu 进行配置,实际系统已经默认配置了大部分,只需要最后的基本步骤的命令。执行时会提示需要删除/var/run/wpa_supplicant/wlan0,先删掉即可。
wpa_passphrase <your_ssid_name> <your_ssid_key> >> /etc/wpa_supplicant.conf
wpa_supplicant -B -iwlan0 -c /etc/wpa_supplicant.conf
iw wlan0 link
这样,就可以连接到外部网络。ping一下百度
三、基本命令测试
1. 查看系统信息 (下面的信息我已经重新编译了kernel)
root@stm32mp2-e3-d6-87:/# uname -a
Linux stm32mp2-e3-d6-87 6.6.116 #2 SMP PREEMPT Wed Mar 25 21:59:12 CST 2026 aarch64 GNU/Linux
2. 查看GCC版本信息
root@stm32mp2-e3-d6-87:/# cat /proc/version
Linux version 6.6.116 (root@ubuntu) (aarch64-ostl-linux-gcc (GCC) 13.4.0, GNU ld (GNU Binutils) 2.42.0.20240723) #2 SMP PREEMPT Wed Mar 25 21:59:12 CST 2026
3. 查看CPU信息命令
root@stm32mp2-e3-d6-87:/# cat /proc/cpuinfo
processor : 0
BogoMIPS : 80.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x1
CPU part : 0xd04
CPU revision : 0
processor : 1
BogoMIPS : 80.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x1
CPU part : 0xd04
CPU revision : 0
BogoMIPS:在系统内核启动时粗略测算的 CPU 每秒运行百万条指令数(MillionInstructionsPerSecond)。
4. 查看 cpu的当前温度
root@stm32mp2-e3-d6-87:/# cat /sys/class/thermal/thermal_zone0/temp
47264
上面显示数字为千分之一度,除以 1000 就是当前温度值,单位为摄氏度。
5. CPU 压力测试
root@stm32mp2-e3-d6-87:/# echo "scale=5000;4*a(1)" | bc -l -q &
上述命令将在后台计算的 PI,并精确到小数点后 5000 位。计算过程需要一段时间,此时,可以通过 top 命令检查 CPU 利用率的变化,如下所示,几乎占用了一个CPU的所有算力
约 2 分钟后,PI 结果被计算出来。在此期间 CPU 单核使用率达满载,没有发生异常,说明 CPU 压力测试通过。还可以继续增加精确值,可进一步提高测试压力。
6. 查看内存信息
root@stm32mp2-e3-d6-87:/# cat /proc/meminfo
MemTotal: 3868068 kB
MemFree: 3453700 kB
MemAvailable: 3484912 kB
Buffers: 15856 kB
Cached: 144852 kB
SwapCached: 0 kB
Active: 48176 kB
Inactive: 172368 kB
Active(anon): 1248 kB
Inactive(anon): 67664 kB
Active(file): 46928 kB
Inactive(file): 104704 kB
Unevictable: 102400 kB
Mlocked: 102400 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 4 kB
Writeback: 0 kB
AnonPages: 162236 kB
Mapped: 41240 kB
Shmem: 9076 kB
KReclaimable: 22928 kB
Slab: 53532 kB
SReclaimable: 22928 kB
SUnreclaim: 30604 kB
KernelStack: 2992 kB
PageTables: 2140 kB
SecPageTables: 0 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 1934032 kB
Committed_AS: 613800 kB
VmallocTotal: 133141626880 kB
VmallocUsed: 16228 kB
VmallocChunk: 0 kB
Percpu: 584 kB
HardwareCorrupted: 0 kB
AnonHugePages: 112640 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
FileHugePages: 0 kB
FilePmdMapped: 0 kB
CmaTotal: 131072 kB
CmaFree: 128376 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
MemTotal:所有可用的 RAM 大小,物理内存减去预留位和内核使用
7. 内存压力测试
通过给定测试内存的大小和次数,可以对系统现有的内存进行压力上的测试。可使用系统工具 memtester 进行测试,如指定内存大小 100MB,测试次数为 1,测试命令为“memtester 100M 1"。这里只是简单测试100MB空间稳定性。
root@stm32mp2-e3-d6-87:/# memtester 100M 1
memtester version 4.6.0 (64-bit)
Copyright (C) 2001-2020 Charles Cazabon.
Licensed under the GNU General Public License version 2 (only).
pagesize is 4096
pagesizemask is 0xfffffffffffff000
want 100MB (104857600 bytes)
got 100MB (104857600 bytes), trying mlock ...locked.
Loop 1/1:
Stuck Address : ok
Random Value : ok
Compare XOR : ok
Compare SUB : ok
Compare MUL : ok
Compare DIV : ok
Compare OR : ok
Compare AND : ok
Sequential Increment: ok
Solid Bits : ok
Block Sequential : ok
Checkerboard : ok
Bit Spread : ok
Bit Flip : ok
Walking Ones : ok
Walking Zeroes : ok
Done.
8. 查看 eMMC 容量
root@stm32mp2-e3-d6-87:/# fdisk -l
Disk /dev/mmcblk2: 7.28 GiB, 7818182656 bytes, 15269888 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: C61F2ECD-0DCD-4DD4-BB20-27D8C38A3BBA
Device Start End Sectors Size Type
/dev/mmcblk2p1 1024 2047 1024 512K unknown
/dev/mmcblk2p2 2048 3071 1024 512K unknown
/dev/mmcblk2p3 3072 11263 8192 4M unknown
/dev/mmcblk2p4 11264 19455 8192 4M unknown
/dev/mmcblk2p5 19456 20479 1024 512K unknown
/dev/mmcblk2p6 20480 151551 131072 64M Linux filesystem
/dev/mmcblk2p7 151552 663551 512000 250M Linux filesystem
/dev/mmcblk2p8 663552 6955007 6291456 3G Linux filesystem
/dev/mmcblk2p9 6955008 15268863 8313856 4G Linux filesystem
Disk /dev/mmcblk2boot0: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk /dev/mmcblk2boot1: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
显示 emmc 大小约为 7.28GB。
8. 查看 eMMC 分区信息
root@stm32mp2-e3-d6-87:~# df -h
Filesystem Size Used Avail Use% Mounted on
devtmpfs 1.8G 0 1.8G 0% /dev
/dev/mmcblk2p8 2.8G 614M 2.1G 23% /
tmpfs 1.9G 0 1.9G 0% /dev/shm
tmpfs 756M 8.9M 747M 2% /run
tmpfs 1.9G 4.0K 1.9G 1% /tmp
/dev/mmcblk2p6 55M 21M 30M 41% /boot
/dev/mmcblk2p7 228M 27M 185M 13% /vendor
/dev/mmcblk2p9 3.8G 64M 3.5G 2% /usr/local
tmpfs 1.9G 28K 1.9G 1% /var/volatile
tmpfs 378M 4.0K 378M 1% /run/user/0
/dev/mmcblk2p8:emmc 根文件系统大小。
9. eMMC 的性能测试
写文件测试
root@stm32mp2-e3-d6-87:/# time dd if=/dev/zero of=test_file bs=10M count=10 conv=fsync
10+0 records in
10+0 records out
104857600 bytes (105 MB, 100 MiB) copied, 1.73408 s, 60.5 MB/s
real 0m 1.80s
user 0m 0.00s
sys 0m 0.63s
读文件测试
root@stm32mp2-e3-d6-87:/# time dd if=test_file of=/dev/null bs=10M count=10 iflag=direct,nonblock
10+0 records in
10+0 records out
104857600 bytes (105 MB, 100 MiB) copied, 1.17674 s, 89.1 MB/s
real 0m 1.18s
user 0m 0.00s
sys 0m 0.18s
由此得出 emmc 的写速度为60.5 MB/s,读速度为 89.1 MB/s。
10. 查看EMMC寿命
root@stm32mp2-e3-d6-87:~# mmc extcsd read /dev/mmcblk2 | grep Life
eMMC Life Time Estimation A [EXT_CSD_DEVICE_LIFE_TIME_EST_TYP_A]: 0x01
eMMC Life Time Estimation B [EXT_CSD_DEVICE_LIFE_TIME_EST_TYP_B]: 0x00
其中关于这些参数的解释:
eMMC Life Time Estimation A:类型 A 主要针对 SLC(单层单元)用户分区的擦除块寿命估计。这种分区通常具有更高的耐久性,适合需要频繁写入的场景。
eMMC Life Time Estimation B:类型 B 则针对 MLC(多层单元)引导分区的擦除块寿命估计。MLC 的耐久性相对较低,但其存储密度更高,适用于需要较大存储空间的应用。
以上参数的值均以 10%的步长参考,例如:0x01 表示使用了 0%-10%的设备寿命。可以看出emmc几乎没有使用。
以上开发板的性能测试。总体下来,这个开发板还是相对给力的。
-
发表了主题帖:
【STM32MP257F-DK测评】开箱测试
本帖最后由 latera 于 2026-3-28 11:28 编辑
非常感谢EEWorld提供的这次STM32MP257F-DK开发板的评测机会。之前学习和工作中。一直使用STM32的MCU来开发项目,算是依靠学习STM32才找到工作的。从接触STM32F4到STM32F7,再到STM32U5系列,但是从来没有用过STM32的MPU系列。而这次可以申请到STM32最为新出stm32mp2的MPU系列芯片的开发板,也特别幸运。
这次评测的stm32mp257f-dk是通过易络盟下单购买的,可能是国外订单,到手的时间比较长。拿到手发现装上SD卡启动不了,先看一下配置,再重新烧录系统。
一、开箱
开发板整体设计的确一眼看上去就是ST的开发,规规整整。
其中开发板的布置如下:
需要留意的是开发板可以通过 usb(cn15),串口(cn21连接板载stlink,并且stlink串口连接stm32mp2),和网口(cn16)连接到的电脑并登录到开发板的系统终端。cn21连接到电脑可以为开发板供电。如果要hdmi(cn1)连接显示屏,最好用电源适配器连接CN21。保证供电可靠。左下方有多个按键和led。其中B1为复位键。LD11为供电指示led。LED1和LD9连接到STM32MP2主芯片上,烧录系统后将闪烁,指示系统启动正常。开发板上方(hdmi接口右侧)的拨码开关用来指示用emmc或者SD卡启动,或者用usb烧录镜像。启动方式如下:
二、烧录镜像
开始想使用的SD烧录镜像,但是发现烧录一半卡住了,改成emmc烧录。两种启动的烧录方式都都一样,都是通过stm32cubeprogramer来完成。可以在https://www.st.com/en/development-tools/stm32cubeprog.html#下载软件,linux和window版本都可以。使用方法一样。stm32mp2的原厂镜像可以在https://www.st.com/en/embedded-software/stm32mp2starter.html#get-software下载.
另外,st给MPU定义了三种不同的开发阶段的开发包:Starter Package,Developer Package,Distribution Package ,其中,starter package就是原厂打包开发镜像,里面有不同的启动方式,不同加密方式,不同开发板的镜像。
将拨码开发拨到usb烧录方式,用2个usb typeC连接电脑,并启动开发板。打开stm32cubeprog。其中usb(cn15)是负责烧录镜像的,cn21在这里只是负责供电。
按如下步骤下载镜像。其中下载到开发板的镜像其实是各个不同的文件组成,包括uboot,内核,文件系统等等,tsv文件就像是这各个镜像文件的目录,指定文件在文件夹的位置。所以第5步中的文件夹路径不能忽略,并且在第4步中选择tsv文件会自动选择文件夹,但是这个文件夹路径可能是不对的。要重新选择。
我一开始使用的是emmc烧录
tsv文件:xxxxx\FLASH-stm32mp2-openstlinux-6.6-yocto-scarthgap-mpu-v26.02.18\stm32mp2-openstlinux-6.6-yocto-scarthgap-mpu-v26.02.18\images\stm32mp2\flashlayout_st-image-weston\optee\FlashLayout_emmc_stm32mp257f-dk-optee.tsv
镜像文件夹:xxxxx\FLASH-stm32mp2-openstlinux-6.6-yocto-scarthgap-mpu-v26.02.18\stm32mp2-openstlinux-6.6-yocto-scarthgap-mpu-v26.02.18\images\stm32mp2
经过很漫长等待,整个镜像才下载完。我接的是usb3.0接口,花了20多分钟。看网上说如果用usb2.0的可能时间需要1小时。
三、demo演示
烧录成功后,选择emmc启动,使用开发板hdmi接口连接显示器,并且将usb鼠标连接到开发板的cn19的usb接口上,重启开发板就可以启动镜像。启动界面和主界面如下:
镜像提供了5个演示功能:
网络数据获取:通过电脑浏览器连接开发板,获取后台数据。连接usb后,开发板会枚举成一个usb ncm设备(虚拟网卡),电路可以通过IP连接开发板。
摄像头演示:以为我没有摄像头,就没有测试了。
视频播放演示
3D图片播放演示
蓝牙连接音乐播放,连接蓝牙耳机,可以播放开发板上的音频文件
以上就是我的开箱测评。下一个评测会测试开发的基本功能和搭建SDK开发环境。
- 2026-03-05
-
回复了主题帖:
测评入围名单:STM32MP257F-DK
个人信息无误,确认可以完成评测计划,已知晓测评文章主打要求
- 2025-12-23
-
发表了主题帖:
【Follow me第三季第3期】 Seeed Studio XIAO ESP32S3 Sense 任务完结汇总
能够参加这个Follow me的活动,非常开心。之前并没有使用ESP32,也没有使用过Arduino来做开发。能够通过这次机会学习之前没有接触过的知识,确实拓宽自己的知识面,以后用到的时候也不至于匆匆忙忙,手忙脚乱。
一、硬件入门和环境搭建
1. 这提供的开发套件包括:
Seeed Studio XIAO ESP32S3一套
MCP4725 DAC输出模块
SH45 温湿度检测模块
其中,Seeed Studio XIAO ESP32S3集成了摄像头传感器、数字麦克风和 SD 卡支持。Xtensa LX7 双核,32 位处理器,运行频率高达 240 MHz。摄像头传感器为OV2640,分辨率 1600x1200。引脚图如下:
原理图可参考如下:
2. 环境搭建
从https://www.arduino.cc/en/software下载最新Arduino的安装包,我使用的2.3.6。启动安装程序,一路点击下一步便可顺利安装,需要注意的是首次打开Arduino。Arduino会继续从网上下载相关组件。因为连接的是外网,这里下载和安装组件的时间可能较长。
导航到 File > Preferences,并在 "Additional Boards Manager URLs" 中填入以下网址:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
导航到 Tools > Board > Boards Manager...,在搜索框中输入关键词 esp32,选择最新版本的 esp32,并安装它。
通过端口选择栏选设备,Seeed Studio XIAO ESP32S3内部虚拟了一个串口设备,在“Select Other Board and Port”中选择板卡和端口。Arduino自动检测板卡是否连接。
二、点亮核心板,串口显示“Hello World”
Seeed Studio XIAO ESP32S3的核心板上带一个led(在usb的一侧)。可以试验一下控制led。因为arduino中已经带有很多例程。这次使用简单的blink的例程。在Arduino上,导航到 File > Examples > 01.Basics > Blink,打开程序。并让如下修改代码:
// the setup function runs once when you press reset or power the board
void setup() {
// initialize digital pin LED_BUILTIN as an output.
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(115200);
Serial.println("Hello World!");
}
// the loop function runs over and over again forever
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
delay(1000); // wait for a second
Serial.println("led on!");
digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
delay(1000); // wait for a second
Serial.println("led off!");
}
流程大致如下:
Arduino 应用窗口可以完成代码的编辑,编译和下载。其中,工具栏上从左往右三个按钮分别为verify,upload,debug。点击verify,表示进行工程检查和编译。点击upload,表示将编译好的固件上传到芯片上。因为Seeed Studio XIAO ESP32S3没有调试功能,debug按钮无法使用。
测试现象如下:
三、使用内置ADC
Seeed Studio XIAO ESP32S3中内置ADC外设,可以读取外部Pin脚的电压。默认使用的Pin脚为A0,XIAO ESP32S3的 A0 对应 D0 引脚。
通过编写代码读取adc值,并将数值转换成对应的电压值。输出到串口中。 如下为示例代码,其中,adc检测值与电压的转换关系为:
实际电压值 = (检测值 / 4095) * 3.3V。
int sensorPin = A0; // select the input pin for the potentiometer
int ledPin = 13; // select the pin for the LED
int sensorValue = 0; // variable to store the value coming from the sensor
void setup() {
// declare the ledPin as an OUTPUT:
pinMode(ledPin, OUTPUT);
Serial.begin(115200);
}
void loop() {
float voltage = 0;
// read the value from the sensor:
sensorValue = analogRead(sensorPin);
voltage = (sensorValue / 4095.0) * 3.3;
Serial.print(voltage);
Serial.println();
// turn the ledPin on
digitalWrite(ledPin, HIGH);
// stop the program for <sensorValue> milliseconds:
delay(200);
sensorValue = analogRead(sensorPin);
voltage = (sensorValue / 4095.0) * 3.3;
Serial.print(voltage);
Serial.println();
// turn the ledPin off:
digitalWrite(ledPin, LOW);
// stop the program for <sensorValue> milliseconds:
delay(200);
}
大致流程为:
实验效果:
可以通过将D0连接到3V3或GND来判断检测电压正确,并且在Arduino的Serial Plotter 窗口查看电压的波形图。另外,在MCP4725 DAC会再次使用到ADC。
四、使用MCP4725 DAC
MCP4725 是一款低功耗、高精度、单通道、12位带缓冲电压输出的数模转换器(DAC),使用I2C通讯。这次活动购买的MCP4725模块已经贴心的将默认I2C地址写在模块之上。并且Arduino实际有挺多支持MCP4725的库。我们可以导入这些库,并在提供的示例中,直接修改代码来测试。
我导入的库为比较简单的MCP4725库。并且使用MCP4725_wave_generator例程来修改。
代码的基本功能为,ESP32通过I2C控制MCP4725模块,对MCP4725初始化配置,根据串口输入的命令,输出对应波形。可以输出正弦波,三角波,方波等,这里主要测试正弦波。为了可以判断输出结果是否正常,将MCP4725的输出引脚接到示波器来查看输出波形,也可以直接接到XIAO模块的D0上,结合上一个实验的代码用ADC同时读取MCP4725的输出电压值。MCP4725主要的代码如下:
MCP4725 MCP(0x62): 设置I2C地址
MCP.begin(): 配置及启动MCP4725
MCP.setValue(xx): 设置当前输出值。
硬件连接:
MCP4725 引脚
XIAO ESP32S3 引脚
VIN
3.3V
GND
GND
SDA
D4
SCL
D5
VOUT
D0
如下图用杜邦线连接:
代码如下:
//
// FILE: MCP4725_wave_generator.ino
// AUTHOR: Rob Tillaart
// PURPOSE: demo function generators
// URL: https://github.com/RobTillaart/MCP4725
// URL: https://github.com/RobTillaart/FunctionGenerator
//
// depending on the platform, the range of "smooth" sinus is limited.
// other signals are less difficult so have a slightly larger range.
//
// PLATFORM SINUS SQUARE SAWTOOTH TRIANGLE
// UNO -100 Hz
// ESP32 -200 Hz -1000 -250 -100
//
#include "MCP4725.h"
#include "Wire.h"
// frequency
// use + - * / to control it
uint16_t freq = 100;
uint32_t period = 0;
uint32_t halvePeriod = 0;
// q = square z = zero
// s = sinus m = mid
// w = sawtooth h = high
// t = stair
// r = random
char waveFrom = 's';
MCP4725 MCP(0x62); // 默认I2C地址为0x62
uint16_t count;
uint32_t lastTime = 0;
int sensorPin = A0; // select the input pin for the potentiometer
int sensorValue = 0; // variable to store the value coming from the sensor
// LOOKUP TABLE SINE
uint16_t sine[361];
void setup()
{
// while(!Serial);
Serial.begin(2000000);
Serial.println(__FILE__);
Serial.print("MCP4725_VERSION: ");
Serial.println(MCP4725_VERSION);
Serial.println();
Wire.begin(); // I2C 初始化
// Wire.setClock(3400000);
// fill table
for (int i = 0; i < 361; i++)
{
sine[i] = 2047 + round(2047 * sin(i * PI / 180));
}
MCP.begin(); // MCP4725 初始化
Wire.setClock(800000);
MCP.setValue(0);
if (!MCP.isConnected())
{
Serial.println("err");
while (1);
}
period = 1e6 / freq;
halvePeriod = period / 2;
while (1)
{
yield();
uint32_t now = micros();
count++;
if (now - lastTime > 100000)
{
lastTime = now;
// show # updates per 0.1 second
// Serial.println(count);
count = 0;
if (Serial.available())
{
int c = Serial.read();
switch (c)
{
case '+':
freq++;
break;
case '-':
freq--;
break;
case '*':
freq *= 10;
break;
case '/':
freq /= 10;
break;
case '0' ... '9':
freq *= 10;
freq += (c - '0');
break;
case 'c':
freq = 0;
break;
case 'A':
break;
case 'a':
break;
case 'q':
case 's':
case 'w':
case 't':
case 'r':
case 'z':
case 'm':
case 'h':
waveFrom = c;
break;
default:
break;
}
period = 1e6 / freq;
halvePeriod = period / 2;
Serial.print(freq);
// Serial.print('\t');
// Serial.print(period);
// Serial.print('\t');
// Serial.print(halvePeriod);
Serial.println();
}
}
uint32_t t = now % period;
switch (waveFrom)
{
case 'q':
if (t < halvePeriod ) MCP.setValue(4095);
else MCP.setValue(0);
break;
case 'w':
MCP.setValue(t * 4095 / period );
break;
case 't':
if (t < halvePeriod) MCP.setValue(t * 4095 / halvePeriod);
else MCP.setValue( (period - t) * 4095 / halvePeriod );
break;
case 'r':
MCP.setValue(random(4096));
break;
case 'z': // zero
MCP.setValue(0);
break;
case 'h': // high
MCP.setValue(4095);
break;
case 'm': // mid
MCP.setValue(2047);
break;
default:
case 's':
// reference
// float f = ((PI * 2) * t)/period;
// MCP.setValue(2047 + 2047 * sin(f));
//
int idx = (360 * t) / period;
MCP.setValue(0.5*sine[idx]); // fetch from lookup table
break;
}
sensorValue = analogRead(sensorPin);
Serial.print("\t");
Serial.print(now);
Serial.print("\t");
Serial.println(sensorValue, 1);
}
}
void loop()
{
}
// -- END OF FILE --
大致流程为:
实验结果:
在Arduino中的Seril Plotter窗口可以看到正弦波的输出
五、连接WiFi,抓取网页信息
ESP32S3支持WIFI,可以连接WIFI路由的网络上,和其他设备通讯或连接到外部网络上。也可以设置为软路由,作为wifi热点,手机或其他设备连接到这热点上。这实验是使用ESP32连接wifi热点,并通过热点登录baidu官网,直接抓取首页内容。来验证ESP32的wifi功能。
大致流程如下:
代码如下:
#include "WiFi.h"
#include <WiFiClient.h>
// Replace with your network credentials
const char* ssid = "xxx"; // 更换你的路由器wifi名称
const char* password = "xxxxxx"; // 更换你的wifi密码
const char* host = "www.baidu.com"; // 目标服务器域名
const int httpPort = 80; // HTTP标准端口
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println();
Serial.println(WiFi.localIP());
}
void sendClientRequest() {
// 建立WiFi客户端对象
WiFiClient client;
// 构造HTTP请求报文
String httpRequest = String("GET /") + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n" +
"\r\n";
Serial.print("正在连接服务器:");
Serial.println(host);
// 尝试连接服务器
if (client.connect(host, httpPort)) {
Serial.println("连接成功!");
// 发送HTTP请求
client.print(httpRequest);
Serial.println("发送的HTTP请求: ");
Serial.println(httpRequest);
Serial.println("服务器响应:");
// 等待并打印所有服务器返回的数据
while (client.connected() || client.available()) {
if (client.available()) {
String line = client.readStringUntil('\n');
Serial.println(line);
}
}
client.stop();
Serial.print("已从服务器断开连接");
} else {
Serial.println("连接服务器失败!");
client.stop();
}
}
void setup() {
Serial.begin(115200);
// Set WiFi to station mode and disconnect from an AP if it was previously connected
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
initWiFi();
sendClientRequest();
}
void loop() {
}
主要代码说明
1. WIFI连接:
WiFi.mode(WIFI_STA) 设置为Station模式,
WiFi.begin(ssid, password) 连接WIFI热点
2. httpclient 使用:
WiFiClient client 声明客户端
client.connect 连接服务端
client.print 发送http请求
client.readStringUntil 读取http响应
client.stop 断开连接。
抓取内容如下可见附件
可以保存成html文件,并通过浏览器来访问这个文件,验证是否和百度首页的内容一致。
六、获取SHT45的温度和湿度信息并显示。
同样SHT45的使用,Arduino也提供相应的库,给我们参考和使用。我下载的是SHT4x的简易库。
SHT45同样通过I2C硬件和ESP32S3连接。硬件连接如下:
SH45 引脚
XIAO ESP32S3 引脚
VIN
3.3V
GND
GND
SDA
D4
SCL
D5
测试也比较简单,通过I2C连接SHT45模块,读取SHT45的温度和湿度值,并通过串口打印这2个值,可以在Arduino上显示这2个值的变化趋势。启动SHT4x库中的默认I2C地址为0x44,和购买的模块的默认地址完全一样。可以不设置SHTx的I2C地址。这样SHTx使用到指令如下:
sht.begin() 初始化
sht.read() 启动读取,并缓存SHT45的寄存器数据
sht.getTemperature() 返回检测温度值
sht.getHumidity() 返回检测湿度值
代码如下:
#include "Wire.h"
#include "SHT4x.h"
#define SHT_DEFAULT_ADDRESS 0x44
uint32_t start;
uint32_t stop;
SHT4x sht;
void setup()
{
// while(!Serial); // uncomment if needed
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("SHT4x_LIB_VERSION: \t");
Serial.println(SHT4x_LIB_VERSION);
Serial.println();
Wire.begin();
Wire.setClock(100000);
sht.begin();
}
void loop()
{
start = micros();
sht.read(); // default SHT4x_MEASUREMENT_SLOW and true for CRC check
stop = micros();
Serial.print("\t");
Serial.print(stop - start);
Serial.print("\t");
Serial.print(sht.getTemperature(), 1);
Serial.print("\t");
Serial.println(sht.getHumidity(), 1);
delay(100);
}
// -- END OF FILE --
大致流程:
实验结果:(为了测试数据是否有变化,我用手轻按住芯片)
七、使用摄像头和麦克风
Seeed Studio XIAO ESP32S3套件的中摄像头 PDM麦克风和SD卡插槽组成独立模组,可和ESP32主板连接在一起。
1. 麦克风使用
PDM麦克风第引脚连接如下,ESP32可以通过I2S控制麦克风实现音频接收。可以将录制的音频,保存在SD卡中。
引脚编号
功能描述
GPIO 41
PDM 麦克风 DATA
GPIO 42
PDM 麦克风 CLK
大致流程如下:
代码如下:
#include "ESP_I2S.h"
#include "FS.h"
#include "SD.h"
void setup() {
// Create an instance of the I2SClass
I2SClass i2s;
// Create variables to store the audio data
uint8_t *wav_buffer;
size_t wav_size;
// Initialize the serial port
Serial.begin(115200);
while (!Serial) {
delay(10);
}
Serial.println("Initializing I2S bus...");
// Set up the pins used for audio input
i2s.setPinsPdmRx(42, 41);
// start I2S at 16 kHz with 16-bits per sample
if (!i2s.begin(I2S_MODE_PDM_RX, 16000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
Serial.println("Failed to initialize I2S!");
while (1); // do nothing
}
Serial.println("I2S bus initialized.");
Serial.println("Initializing SD card...");
// Set up the pins used for SD card access
if(!SD.begin(21)){
Serial.println("Failed to mount SD Card!");
while (1) ;
}
Serial.println("SD card initialized.");
Serial.println("Recording 20 seconds of audio data...");
// Record 20 seconds of audio data
wav_buffer = i2s.recordWAV(20, &wav_size);
// Create a file on the SD card
File file = SD.open("/arduinor_rec.wav", FILE_WRITE);
if (!file) {
Serial.println("Failed to open file for writing!");
return;
}
Serial.println("Writing audio data to file...");
// Write the audio data to the file
if (file.write(wav_buffer, wav_size) != wav_size) {
Serial.println("Failed to write audio data to file!");
return;
}
// Close the file
file.close();
Serial.println("Application complete.");
}
void loop() {
delay(1000);
Serial.printf(".");
}
其中,主要有2部分操作,一个是PDM设置和数据PDM数据。一个是SD的文件操作。
i2s.setPinsPdmRx(42, 41),设置PIN模式为PDM接收。
i2s.begin(I2S_MODE_PDM_RX, 16000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),设置I2S格式为PDM接收,16K采样率,16bit位宽,单声道。
i2s.recordWAV(20, &wav_size), 录制20s的wav格式音频。
SD.begin 启动SD卡。
SD.open("/arduinor_rec.wav", FILE_WRITE) 打开或创建文件
file.write(wav_buffer, wav_size) != wav_size) 将wav音频数据写入文件。
注意的是,我们需要使用 ESP-32 芯片的 PSRAM 功能,主要用于保存实验中的20s录音数据。可以在Tool中打开:
测试效果(可以查看录制的视频)
2. 摄像头使用
Seeed Studio XIAO ESP32S3的摄像头传感器为OV2640,分辨率 1600x1200。摄像头占用ESP32的14个PIN脚。具体如下:
ESP32-S3 GPIO
摄像头
ESP32-S3 GPIO
摄像头
GPIO10
XMCLK
GPIO11
DVP_Y8
GPIO12
DVP_Y7
GPIO13
DVP_PCLK
GPIO14
DVP_Y6
GPIO15
DVP_Y2
GPIO16
DVP_Y5
GPIO17
DVP_Y3
GPIO18
DVP_Y4
GPIO38
DVP_VSYNC
GPIO39
CAM_SCL
GPIO40
CAM_SDA
GPIO47
DVP_HREF
GPIO48
DVP_Y9
esp32的摄像头主要使用的指令如下:
esp_err_t esp_camera_init(const camera_config_t* config); 初始化摄像头
camera_fb_t* esp_camera_fb_get(); 获取指向帧缓冲区的指针,用于读取当前帧内容
void esp_camera_fb_return(camera_fb_t * fb); 返回帧缓冲区以便再次使用
实验的大致流程和麦克风的测试一样,大致流程就是将获取的图像保存到SD中。详细代码见附件。
#include "esp_camera.h"
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#define CAMERA_MODEL_XIAO_ESP32S3 // Has PSRAM
#include "camera_pins.h"
unsigned long lastCaptureTime = 0; // Last shooting time
int imageCount = 1; // File Counter
bool camera_sign = false; // Check camera status
bool sd_sign = false; // Check sd status
// Save pictures to SD card
void photo_save(const char * fileName) {
// Take a photo
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Failed to get camera frame buffer");
return;
}
// Save photo to file
writeFile(SD, fileName, fb->buf, fb->len);
// Release image buffer
esp_camera_fb_return(fb);
Serial.println("Photo saved to file");
}
// SD card write file
void writeFile(fs::FS &fs, const char * path, uint8_t * data, size_t len){
Serial.printf("Writing file: %s\r\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.write(data, len) == len){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
void setup() {
Serial.begin(115200);
while(!Serial); // When the serial monitor is turned on, the program starts to execute
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.frame_size = FRAMESIZE_UXGA;
config.pixel_format = PIXFORMAT_JPEG; // for streaming
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12;
config.fb_count = 1;
// if PSRAM IC present, init with UXGA resolution and higher JPEG quality
// for larger pre-allocated frame buffer.
if(config.pixel_format == PIXFORMAT_JPEG){
if(psramFound()){
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
// Limit the frame size when PSRAM is not available
config.frame_size = FRAMESIZE_SVGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}
} else {
// Best option for face detection/recognition
config.frame_size = FRAMESIZE_240X240;
#if CONFIG_IDF_TARGET_ESP32S3
config.fb_count = 2;
#endif
}
// camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
camera_sign = true; // Camera initialization check passes
// Initialize SD card
if(!SD.begin(21)){
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
// Determine if the type of SD card is available
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
Serial.print("SD Card Type: ");
if(cardType == CARD_MMC){
Serial.println("MMC");
} else if(cardType == CARD_SD){
Serial.println("SDSC");
} else if(cardType == CARD_SDHC){
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
sd_sign = true; // sd initialization check passes
Serial.println("Photos will begin in one minute, please be ready.");
}
void loop() {
// Camera & SD available, start taking pictures
if(camera_sign && sd_sign){
// Get the current time
unsigned long now = millis();
//If it has been more than 1 minute since the last shot, take a picture and save it to the SD card
if ((now - lastCaptureTime) >= 60000) {
char filename[32];
sprintf(filename, "/image%d.jpg", imageCount);
photo_save(filename);
Serial.printf("Saved picture: %s\r\n", filename);
Serial.println("Photos will begin in one minute, please be ready.");
imageCount++;
lastCaptureTime = now;
}
}
}
拍照效果如下:
八、视频流测试
最后,可以通过 XIAO ESP32S3 Sense 创建的网页上查看实时视频流。这个项目来自CameraWebServer_for_esp-arduino_3.0.x
这个项目是一个综合的例子,用到ESP32S3的wifi和摄像头功能,并且它还有一个人脸识别的功能,但是我只是演示和了解了代码的大概功能。并没有深入学习。
总结
通过这次学习,了解到使用XIAO ESP32S3 Sense在arduino上开发的流程。arduino开发的确很便捷,里面有很多使用的库,像wifi,摄像头这些外设,可以轻松配置和使用。而像SHT45等这些小模块,就更方便了,也不需要先学习模块的芯片手册,只需要调用适当的库api就可以。这对于小白或者希望快速出成效的开发者都比较友好。
而 XIAO ESP32S3 Sense这个开发板小巧精致,但又五脏俱全。上面的wifi,摄像头,pdm麦克风都有相应的例程和学习资料。这次学习过程也很顺利。
视频链接如下:
代码链接如下:download.eeworld.com.cn/detail/latera/637870
-
上传了资料:
[Follow me第三季第3期】 Seeed Studio XIAO ESP32S3 Sense 任务完结汇总
- 2025-12-17
-
回复了主题帖:
【Stratasys有奖直播】如何通过3D打印稳定交付小批量工程零件?
报名
- 2025-09-07
-
上传了资料:
【Follow me第三季第2期】任务完结汇总
-
回复了主题帖:
【回帖赢50元京东卡】机器人技术讨论篇(9月新活动)
NanoEdgeAIStudio:面向 STM32 开发者的自动化机器学习工具 - DigiKey 电子百科 - DigiKey 技术论坛 - 电子物料和组件方案的专业技术论坛
- 2025-08-24
-
发表了主题帖:
[Follow me第三季第2期】任务完结汇总
本帖最后由 latera 于 2025-9-13 17:25 编辑
任务汇总
首先再次感谢digikey的Follow me活动提供的这次学习MCXN947开发板的机会。在这次活动中,我对N947的基础架构,硬件外设都有一定的了解。并且通过尝试在MCUXpresso IDE上开发程序。而且尝试了恩智浦提供的AI实例,进一步学习到深度学习在嵌入式开发中的应用。
任务介绍
[localvideo]bc6c935be852430ff6f7be9763ccbe23[/localvideo]
任务说明
以下为我在论坛先后提交作品:
1. MCUXpresso IDE的环境搭建,开发板实现点灯和串口打印的基本功能。
【Follow me第三季第2期】搭建环境 调试示例程序 hello_world + 添加led_blinky
(1)硬件
(2)功能说明
程序相对简单,主要通过串口打印信息。和通过3个GPIO控制板载上的三色灯。程序中通过简单的延时,切换三个灯的高低电平,来实现灯光闪烁的效果。
(3)学习心得
通过这次了解MCUxpresso开发环境和例程的导入方法。并且了解串口和GPIO的基本使用。
2. 通过串口实现控制台功能,还通过板载的P3T1755DP温度传感器获取环境温度。并且在文章中详细了解的N947启动文件结构和启动流程。
【Follow me第三季第2期】shell命令例程测试+温度获取
(1)硬件
(2)功能说明
文章详细分析了N947从上电到进入main函数的启动步骤。
从电路图开始说明串口的配置方法,并且分析示例中uart读写操作 和 调试打印输出输入的嵌套关系。
根据shell例程的步骤,实现在串口终端输入获取温度的命令后,在终端显示当前传感器检测的温度。传感器为P3T1755DP。使用I3C接口通讯。
(3)学习心得
了解到N947的M33启动,和之前cortex M4的启动方式基本相同。并对更熟悉了解到SDK配置外设的方法。
3. 恩智浦的AI例程测试N947的NPU功能。
Follow me第三季第2期】CIFAR10图像识别方案NPU测试
(1)硬件
(2)功能说明
这个例程使用Softmax分类器深度学习图像分类方法,数据集为CIFAR10,是例程训练好的,但也可用eiq软件,添加需要区分的物体图片,来重新训练出新的模型。
程序大致流程如下,将摄像头获取到的图片数据传给模型处理器,最终将结果显示在lcd上。
(3)学习心得
测试N947的LCD显示和摄像头数据获取的功能,基本了解的N947的机器学习上的应用。当然感觉这个图像识别的模型识别能力并不是很好。部分图片要识别多次才能识别正确。
4. 这是一个测试NPU的深度学习的Demo。使用MPU6050获取风扇转动时的六轴数据,通过训练模型了判断风扇是否异常。
【Follow me第三季第2期】风扇异常检测方案测试
(1)硬件
(2)功能说明
程序使用的是支持向量机深度模型来区分风扇是否正常运行,采集和分析数据来之MPU6050的六轴加速度角速度检测数据。并且程序支持在线训练数据。大致流程如下:
(3)学习心得
学习到N947在线训练模型和模型推理的功能。对支持向量机(support vector machines, SVM)这种深度学习模型有了一定了解。
代码提交
代码同样上传到了
- 2025-08-07
-
评论了课程:
EEWORLD大学堂----直播回放: 利用MPS在线仿真软件轻松掌握电源环路参数设计
谢谢分享
- 2025-08-05
-
回复了主题帖:
Follow me第三季第2期】CIFAR10图像识别方案NPU测试
xingkong911 发表于 2025-8-4 20:56
从Application Code Hub上面下的工程可以运行, 但是更新了训练数据提示不兼容,
请问你的SDK,eIQ ...
MCXN947: How to Train and Deploy Customer ML model to NPU - NXP Community 训练数据可以看这个,里面有个视频
- 2025-08-02
-
回复了主题帖:
Follow me第三季第2期】CIFAR10图像识别方案NPU测试
xingkong911 发表于 2025-8-1 22:01
把这三个地方改了之后, LCD确实好了
但识别率还是有点低,不知道自带工程训练得不够还是怎么着
自带工程的确识别率不高,特别是青蛙
- 2025-07-28
-
发表了主题帖:
【Follow me第三季第2期】风扇异常检测方案测试
本帖最后由 latera 于 2025-7-28 23:08 编辑
本文是测试NXP中的官方AI例程,这个例程是通过6轴姿态传感器来检查风扇转动,并通过支持向量机的机器模型来判断风扇是否异常,比如说停止转动,或是转动卡顿的异常情况。而且,这个例程的模型是直接在端侧,即N947开发板上训练模型。
以下为需要使用到的硬件
LCD显示屏 LCD-PAR-S035
MPU60506轴姿态传感器模块
FRDM-MCXN947开发板
电脑
小风扇
硬件连接
MPU6050的6轴姿态传感器模块是在网上购买。实际本例程中支持2中6轴姿态传感器,一个是MPU6050,另外还有FXLS8974CF。FXLS8974CF是NXP自己出品的传感器芯片,但是网上能购买到的现成模块比较少,而MPU6050就很常见。所有就直接选择了MPU6050。不过,无论是MPU6050还是FXLS89CF,都是使用板载的I2C接口,所以2种模块的连接到核心板的方式都一样。以下是我购买到的模块。
LCD模块依然使用NXP的开源硬件 LCD-PAR-S035显示屏。
硬件连接方式如下图,MPU6050的 VCC,GND,SCL,SDA,四个引脚分别通过杜邦线连接到开发板上的J9排座的(pin21,22,19,20)即可。
需要注意的是,因为开发板上的SDA和SCL连接上拉电阻(4.7k),6050模块是上也连接了上拉电阻,可能会影响通讯,所有我直接将模块上的上拉电阻去掉。
例程下载和编译
这个同样可以MCUXpresso IDE中的“Import from Application Code Hub...”直接导入。导入方式可以参考我上一篇文章。
也可以直接在github上下载,链接如下:
https://www.github.com/NXP-APPCODEHUB/dm-on-device-training-fan-anomaly-on-mcxn947.git
下载后,工程如下:
同样,因为编译选项默认要求数据格式显式转换,和函数显式声明,不然编译报错。
可以按如下修改,修改内容在右侧
其中实际主要修改的只有main.c文件。修改后便可以编译成功。
测试发现,有时候,MPU6050初始化时不能正常读取6050的ID寄存器。所以我直接将判断ID的步骤去掉。如下修改mpu6050.c的代码:
u8 MPU_Init(void)
{
u8 res;
MPU_IIC_Init();
MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X81);
u8 mgmt1 = MPU_Read_Byte(MPU_PWR_MGMT1_REG);
delay_ms(100);
MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X00);
MPU_Set_Gyro_Fsr(3);
MPU_Set_Accel_Fsr(0);
MPU_Set_Rate(SAMPLE_RATE);
MPU_Write_Byte(MPU_INT_EN_REG,0X00);
MPU_Write_Byte(MPU_USER_CTRL_REG,0X00);
MPU_Write_Byte(MPU_FIFO_EN_REG,0X00);
MPU_Write_Byte(MPU_INTBP_CFG_REG,0X80);
res=MPU_Read_Byte(MPU_DEVICE_ID_REG);
// if(res==MPU_ADDR) // 去除判断。
// {
MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X01);
MPU_Write_Byte(MPU_PWR_MGMT2_REG,0X00);
MPU_Set_Rate(SAMPLE_RATE);
// }else return 1;
MPU_Set_Fifo(ACCEL_FIFO_EN);
return 0;
}
实际程序中的主要步骤如下,程序开始新建三个主要任务,分别为是lvgl显示和触控处理的AppTask,负责算法的模型训练和结果预测的app_algo_task,还是完成传感器数据采集的app_sensor_task。程序主要对应2大功能:模型训练和结果预测显示。
示例演示
模型训练
[localvideo]f1ece599bfe1b82435e970737d3b9476[/localvideo]
风扇正常和异常预测结果演示
[localvideo]e763a17536d70b01d50ebeb4c4dea64f[/localvideo]
例程的AI模型说明
本例程中使用的模型为支持向量机(support vector machines, SVM)。支持向量机是一种二分类模型,它的基本模型是定义在特征空间上的间隔最大的线性分类器。在本文的功能就是将区分风扇异常和正常,而对应的特征就是6轴姿态传感器的3轴加速度和3轴角速度。这6个数据(特征)组成一个向量(点)。模型通过收集这些大量的数据,并且标识出正常点和异常点,并找到一个最优超平面,将不同类别(本文中正常的6轴数据和异常6轴数据)的数据分开。在预测时,对获取到的新数据进行判断,是在超平面哪一侧。
并且,本文使用的是One-Class SVM 的异常检测算法。One-Class SVM是一种无监督算法,我理解就是不用外部去标识那些是异常点,那些是正常点,由模型自行判断,模型在训练中根据不断输入的数据,判断新进入的数据是否和之前以及采集到的数据是否区别很大,从而判断是否为异常点。
训练中,如何判断新采集是否区别很大,还是很小,就需要通过模型训练中2个超参数的设定:
Gamma
这个参数会将数据集参数理解成多个数据簇,如风扇有2个档位,应该收集到的数据会趋向于2个范围。Gamma越大就越趋向于将数据理解成多个数据范围。
Nu
代表模型训练时的采集数据的灵敏度。Nu越大,训练数据中越容易得出异常数据。
以上学习内容,只是我对机器学习初浅的理解。还要继续学习才行。。