- 2024-11-27
-
回复了主题帖:
免费下载 | 安森美电动汽车充电白皮书,看碳化硅如何缓解“里程焦虑”!
已经参加
- 2024-11-11
-
发表了主题帖:
STM32H7S78-DK 开发套件三周目评测:简单声音采集保存之SD 卡录音保存和状态显示
最后一个测试周目是基于 STM32H7 微控制器的音频采集与处理系统,能够采集音频信号,并将其以 WAV 格式保存至 SD 卡。采用 LED 闪烁来为用户提供各种状态反馈,包括录音状态、错误信息等。
二、项目模块结构
(一)错误处理与 LED 显示模块
功能:统一处理系统中的各类错误情况,并通过控制 LED 的不同闪烁模式向用户反馈相应信息。同时,也用于设置正常录音状态下的 LED 显示模式。
代码实现:
// 定义不同的闪烁模式
#define LED_OFF 0
#define LED_ON 1
#define LED_BLINK_FAST 2
#define LED_BLINK_SLOW 3
#define LED_ALTERNATE_BLINK 4
// 定义错误码对应的闪烁模式
#define ERROR_MOUNT_SD_CARD LED_BLINK_FAST
#define ERROR_OPEN_FILE LED_BLINK_FAST
#define ERROR_ADC_DMA_INIT LED_BLINK_FAST
#define RECORDING_ON LED_ON
#define RECORDING_OFF LED_OFF
#define ERROR_LONG_PRESS LED_BLINK_FAST
// 错误处理函数,通过LED闪烁显示错误
void handleError(uint8_t errorMode) {
switch (errorMode) {
case ERROR_MOUNT_SD_CARD:
case ERROR_OPEN_FILE:
case ERROR_ADC_DMA_INIT:
case ERROR_LONG_PRESS:
// 设置LED闪烁模式来表示错误
setLEDMode(errorMode);
break;
default:
break;
}
}
// 设置LED的闪烁模式
void setLEDMode(uint8_t mode) {
switch (mode) {
case LED_OFF:
HAL_GPIO_WritePin(GPIO_PIN_1, GPIO_PIN_RESET); // LD1(绿色)关闭
HAL_GPIO_WritePin(GPIO_PIN_5, GPIO_PIN_RESET); // LD2(橙色)关闭
HAL_GPIO_WritePin(GPIO_PIN_2, GPIO_PIN_SET); // LD3(红色)关闭
break;
case LED_ON:
if (mode == RECORDING_ON) {
HAL_GPIO_WritePin(GPIO_PIN_1, GPIO_PIN_SET); // LD1(绿色)打开,表示录音中
HAL_GPIO_WritePin(GPIO_PIN_5, GPIO_PIN_RESET); // LD2(橙色)关闭
HAL_GPIO_WritePin(GPIO_PIN_2, GPIO_PIN_SET); // LD3(红色)关闭
}
break;
case LED_BLINK_FAST:
// 实现快速闪烁逻辑(使用LD2橙色LED)
for (int i = 0; i < 10; i++) {
HAL_GPIO_TogglePin(GPIO_PIN_5, GPIO_PIN_SET);
HAL_Delay(100);
}
break;
case LED_BLINK_SLOW:
// 实现慢速闪烁逻辑(使用LD2橙色LED)
for (int i = 0; i < 5; i++) {
HAL_GPIO_TogglePin(GPIO_PIN_5, GPIO_PIN_SET);
HAL_Delay(500);
}
break;
case LED_ALTERNATE_BLINK:
// 实现交替闪烁逻辑
for (int i = 0; i < 5; i++) {
HAL_GPIO_TogglePin(GPIO_PIN_1, GPIO_PIN_SET);
HAL_GPIO_TogglePin(GPIO_PIN_5, GPIO_PIN_SET);
HAL_Delay(300);
}
break;
}
}
在这个模块中,handleError函数接收错误模式参数,并调用setLEDMode函数来控制 LED 的闪烁模式。不同的错误或状态对应不同的闪烁模式,如ERROR_MOUNT_SD_CARD等错误码对应LED_ALTERNATE_BLINK模式,而录音开启状态RECORDING_ON对应LED_ON模式,通过LED1(红灯)和LED2(绿灯)的亮灭或LED3(用于特殊状态显示)的闪烁来向用户传达信息。
(二)音频采集模块
功能:利用 STM32 的 ADC(轮询方式)启动音频信号采集过程。
代码实现:
// 初始化音频采集
void InitAudioCapture() {
if (HAL_ADC_Start(&hadc1)!= HAL_OK) {
handleError(ERROR_ADC_START);
}
}
InitAudioCapture函数用于初始化音频采集功能。它主要负责启动 STM32 芯片上的模数转换器(ADC)。如果 ADC 启动成功,那么后续就可以开始采集音频信号;如果启动失败,则通过调用handleError函数来处理错误,这里的错误处理方式是根据预定义的错误模式(ERROR_ADC_START)来控制 LED 闪烁,向用户提示 ADC 启动出现问题。
(三)WAV 文件生成模块
功能:生成符合 WAV 格式标准的文件头。
代码实现:
// 生成WAV文件头
void generateWavHeader(FIL *file) {
uint32_t dataSize = 0;
uint32_t byteRate = SAMPLE_RATE * NUM_CHANNELS * (BITS_PER_SAMPLE / 8);
uint32_t blockAlign = NUM_CHANNELS * (BITS_PER_SAMPLE / 8);
// RIFF 块
const char riffHeader[] = {'R', 'I', 'F', 'F'};
f_write(file, riffHeader, sizeof(riffHeader), NULL);
f_write(file, &dataSize, sizeof(dataSize), NULL);
const char waveFormat[] = {'W', 'A', 'V', 'E'};
f_write(file, waveFormat, sizeof(waveFormat), NULL);
// fmt 子块
const char fmtHeader[] = {'f', 'm', 't', ' '};
f_write(file, fmtHeader, sizeof(fmtHeader), NULL);
const uint32_t fmtChunkSize = 16;
f_write(file, &fmtChunkSize, sizeof(fmtChunkSize), NULL);
const uint16_t audioFormat = 1;
f_write(file, &audioFormat, sizeof(audioFormat), NULL);
f_write(file, &NUM_CHANNELS, sizeof(NUM_CHANNELS), NULL);
f_write(file, &SAMPLE_RATE, sizeof(SAMPLE_RATE), NULL);
f_write(file, &byteRate, sizeof(byteRate), NULL);
f_write(file, &blockAlign, sizeof(blockAlign), NULL);
f_write(file, &BITS_PER_SAMPLE, sizeof(BITS_PER_SAMPLE), NULL);
// data 子块
const char dataHeader[] = {'d', 'a', 't', 'a'};
f_write(file, dataHeader, sizeof(dataHeader), NULL);
f_write(file, &dataSize, sizeof(dataSize), NULL);
}
该模块根据定义好的音频参数(如采样率SAMPLE_RATE、声道数NUM_CHANNELS、每个样本的位数BITS_PER_SAMPLE)生成 WAV 文件头。此函数在录音保存到 SD 卡的过程中被调用。
(四)录音保存到 SD 卡模块
功能:将采集到的音频数据保存为 WAV 格式文件到 SD 卡,并以日期时间命名文件,同时处理文件操作过程中的错误情况。
代码实现:
此模块首先尝试挂载 SD 卡文件系统,在这一过程中若挂载失败(即f_mount函数返回值不为FR_OK),则通过handleError函数以ERROR_MOUNT_SD_CARD对应的 LED 闪烁模式向用户提示错误信息。挂载成功后,接着会根据当前日期和时间来创建文件名。之后尝试打开此文件,若文件打开操作失败(也就是f_open函数返回值不为FR_OK),同样会调用handleError函数,同时卸载文件系统。当文件成功打开后,会生成符合 WAV 格式的文件头。在录音过程中,使用轮询 ADC 转换是否完成,完成后获取其值并将音频数据逐个写入文件。待录音结束后,根据已写入文件的音频数据量来更新 WAV 文件头中的数据大小信息,最后关闭文件,并卸载 SD 卡文件系统。
(五)按键控制模块
功能:实现按键防抖功能,并通过按键控制录音的开始、停止、暂停等操作,同时在出现异常按键情况(如长按)时通过 LED 显示错误信息。
代码实现:
// 更完善的按键防抖和长按检测
#define DEBOUNCE_COUNT 5
#define LONG_PRESS_TIME 1000 // 长按时间阈值(单位:毫秒)
uint8_t buttonDebounceCounter = 0;
uint32_t buttonPressStartTime = 0;
// 假设这里的 BUTTON_Pin 就是控制 B2 按键的引脚(PC13)
#define BUTTON_Pin GPIO_PIN_13
#define BUTTON_PORT GPIOC
uint8_t debounceButton(uint16_t GPIO_Pin) {
uint8_t currentState = HAL_GPIO_ReadPin(BUTTON_PORT, BUTTON_Pin); // 读取B2按键引脚状态
if (currentState == button.previousState) {
if (currentState == GPIO_PIN_SET) { // 按下为高电平
buttonDebounceCounter++;
if (buttonDebounceCounter >= DEBOUNCE_COUNT) {
if (HAL_GetTick() - buttonPressStartTime > LONG_PRESS_TIME) {
handleError(ERROR_LONG_PRESS);
return GPIO_PIN_SET; // 返回高电平表示长按
}
}
} else {
buttonDebounceCounter = 0;
buttonPressStartTime = HAL_GetTick();
}
} else {
buttonDebounceCounter = 0;
button.previousState = currentState;
}
return 0;
}
// 按键中断处理
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == BUTTON_Pin && debounceButton(GPIO_Pin) == GPIO_PIN_SET) { // 检测到B2按下
switch (buttonAction) {
case BUTTON_PRESS_SHORT:
recording =!recording;
setLEDMode(recording? RECORDING_ON : RECORDING_OFF);
if (recording) {
SaveAudioToSDCard();
}
break;
default:
break;
}
}
}
在debounceButton函数中,实现了按键防抖和长按检测功能。当检测到按键按下且经过防抖处理后,如果按键按下时间超过LONG_PRESS_TIME阈值,判定为长按操作,调用handleError函数以ERROR_LONG_PRESS对应的 LED 闪烁模式提示用户。在HAL_GPIO_EXTI_Callback函数中,处理短按按键操作,根据按键状态切换录音状态,并通过setLEDMode函数设置相应的 LED 显示模式(录音开启时为RECORDING_ON模式,录音停止时为RECORDING_OFF模式),同时在录音开始时调用SaveAudioToSDCard函数开始保存音频。
实际操作时候,保存文件一直有问题,保存的文件打开音频有些失真,应该是采样率的问题,还需要修改调试一下。。。
- 2024-11-07
-
回复了主题帖:
# STM32H7S78-DK 开发套件三周目评测:简单声音采集保存之使用 SD 卡读写的实现与分析
cc1989summer 发表于 2024-11-2 13:03
哥们,你这帖子在那干讲,没开发板跑的实物图啊。
后面加,哈哈哈
- 2024-11-01
-
回复了主题帖:
《STM32H7S78-DK 开发套件二周目评测:简单声音采集之驱动MAX4466高精度声音传感器》
cc1989summer 发表于 2024-10-30 21:46
哥们,翻到开发板背面,靠近USB1上面,有个芯片U1:MP23DB01,就是数字麦克风
o.o!!!我看手册是写有麦克风,但是又看示意图写的是一个麦克风接口。原来是这样!
- 2024-10-31
-
发表了主题帖:
# STM32H7S78-DK 开发套件三周目评测:简单声音采集保存之使用 SD 卡读写的实现与分析
该项目包括了保存录音、按键控制、串口控制、频率成分分析与分类等功能,逐步进行完成。
首先进行sd读写的开。在嵌入式开发中,尤其在无操作系统的裸机环境下,SD卡是实现大容量数据存储的重要组件。
## 1. 硬件连接
在开始CubeMX配置前,需要先了解引脚的配置。
### 1.1 数据引脚连接
- **D0 - D3**:将SD卡的数据引脚D0-D3分别连接到STM32的PC8、PC9、PC10、PC11。
- **CMD**:将SD卡的CMD引脚连接到STM32的PD2,用于命令传输。
- **CLK**:将SD卡的CLK引脚连接到STM32的PC12,提供时钟信号。
- **SD_Detect**:将SD卡的SD_Detect引脚连接至GPIO(PM14),用于检测SD卡插入状态。
具体连接示例代码:
```c
// 配置引脚,在CubeMX中生成的初始化代码中可见
void MX_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
// Enable clock for GPIOC and GPIOD
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOM_CLK_ENABLE(); // 使能PM14引脚所在的GPIO时钟
// 配置SDMMC引脚:CMD (PD2), CLK (PC12), D0 (PC8), D1 (PC9), D2 (PC10), D3 (PC11)
GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF12_SDMMC1;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
// 配置SD_Detect引脚 (PM14)
GPIO_InitStruct.Pin = GPIO_PIN_14;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉电阻
HAL_GPIO_Init(GPIOM, &GPIO_InitStruct);
}
```
## 2. CubeMX 配置与代码实现
CubeMX能够简化SD卡和文件系统的配置,我们将通过它来生成SD卡的初始化代码,并使用FATFS文件系统以实现文件操作。这里选择FATFS文件系统方式除了是可以直接配置简单快捷外,使用eprom还有一些缺点:
1.没有文件系统,sd卡在电脑等设备无法读取。
2.读写速度慢、读写区域小。
### 2.1 CubeMX 配置步骤
#### 2.1.1 SDMMC 接口设置
1. 在CubeMX中打开**Peripherals -> SDMMC1**接口。
2. 将其模式设置为**4-bit**模式,以提升数据传输速率。
3. 在**Clock Configuration**中将SDMMC1的时钟频率调整为48MHz,以保证传输速率和稳定性。
#### 2.1.2 启用 FATFS 文件系统
1. 在**Middlewares**中启用FATFS支持,选择SDMMC1作为接口,以实现基于文件系统的操作。
2. 生成初始化代码,其中`fatfs.c`文件包含了文件系统初始化和读写的基本接口。
### 2.2 基本代码示例
接下来使用FATFS文件系统在SD卡上进行文件创建、写入和读取:
```c
#include "fatfs.h"
#include "sdmmc.h"
#include
void SD_Test() {
FATFS fs;
FIL file;
FRESULT res;
UINT bw;
// 挂载文件系统
res = f_mount(&fs, "0:", 1);
if (res == FR_OK) {
// 创建并写入文件
res = f_open(&file, "0:/test.txt", FA_CREATE_ALWAYS | FA_WRITE);
if (res == FR_OK) {
char writeData[] = "Hello, SD Card!";
f_write(&file, writeData, sizeof(writeData), &bw);
f_close(&file);
}
// 读取文件
res = f_open(&file, "0:/test.txt", FA_READ);
if (res == FR_OK) {
char readData[32];
f_read(&file, readData, sizeof(readData), &bw);
printf("Read data: %s\n", readData);
f_close(&file);
}
// 卸载文件系统
f_mount(NULL, "0:", 1);
}
}
```
## 3. 不同 SD 卡读写方式与代码分析
当然在不同场景和需求下,对sd卡的读写操作应该有适当改变,才能提高效率、节省资源。
### 3.1 基于文件系统的标准读写
- **优点**:CubeMX自动生成FATFS代码,简化了文件操作流程,适用于低频存储。比如传感器的数据记录等。
- **缺点**:由于文件系统需要索引和管理结构,写入速度会受影响。
```c
void SD_WriteRead_FileSystem() {
FATFS fs;
FIL file;
UINT bw, br;
char writeData[] = "File System Write Test";
char readData[32];
// 挂载文件系统
f_mount(&fs, "0:", 1);
// 写入文件
f_open(&file, "0:/filesystem.txt", FA_CREATE_ALWAYS | FA_WRITE);
f_write(&file, writeData, sizeof(writeData), &bw);
f_close(&file);
// 读取文件
f_open(&file, "0:/filesystem.txt", FA_READ);
f_read(&file, readData, sizeof(readData), &br);
f_close(&file);
printf("Read Data: %s\n", readData);
f_mount(NULL, "0:", 1);
}
```
### 3.2 扇区级块读写
- **优点**:直接操作扇区避免了文件系统开销,适合高频数据写入。比如音视频、网络文件等。
- **缺点**:需要手动管理数据位置和扇区分配,编程难度大。
```c
void SD_WriteBlock(uint32_t sector, uint8_t *data, uint32_t count) {
if (HAL_SD_WriteBlocks(&hsd1, data, sector, count, HAL_MAX_DELAY) == HAL_OK) {
printf("Block write success!\n");
}
}
void SD_ReadBlock(uint32_t sector, uint8_t *data, uint32_t count) {
if (HAL_SD_ReadBlocks(&hsd1, data, sector, count, HAL_MAX_DELAY) == HAL_OK) {
printf("Block read success!\n");
}
}
```
### 3.3 缓存 + 文件系统混合模式
- **优点**:使用缓存减少频繁写入,提升写入效率,适合音频等连续数据存储。
- **缺点**:占用较多RAM资源,需考虑系统内存限制。
```c
#define BUFFER_SIZE 512
uint8_t cacheBuffer[BUFFER_SIZE];
void SD_WriteCache(const char *data) {
static uint32_t offset = 0;
while (*data) {
cacheBuffer[offset++] = *data++;
if (offset >= BUFFER_SIZE) {
SD_WriteBlock(0, cacheBuffer, BUFFER_SIZE / 512); // 以块为单位写入
offset = 0;
}
}
}
```
## 4. 音频数据存储的优化方案
考虑到录音是一个时效性很强的操作,所以对存储效率要求很高,下面是两个优化的方案。
### 4.1 双缓冲技术
设置两个缓冲区,交替写入SD卡以减小写入延时。
```c
uint8_t bufferA[BUFFER_SIZE];
uint8_t bufferB[BUFFER_SIZE];
volatile uint8_t *currentBuffer = bufferA;
void Audio_WriteToSD() {
if (currentBuffer == bufferA) {
SD_WriteBlock(0, bufferA, BUFFER_SIZE / 512);
currentBuffer = bufferB;
} else {
SD_WriteBlock(0, bufferB, BUFFER_SIZE / 512);
currentBuffer = bufferA;
}
}
```
### 4.2 文件分段存储
音频数据按时间或大小分段存储,避免大文件操作带来的开销。
```c
void SaveAudioSegment(uint8_t *data, size_t size, uint8_t segmentNum) {
char filename[20];
sprintf(filename, "
audio_segment_%d.wav", segmentNum);
f_open(&file, filename, FA_CREATE_ALWAYS | FA_WRITE);
f_write(&file, data, size, &bw);
f_close(&file);
}
```
- 2024-10-30
-
回复了主题帖:
《STM32H7S78-DK 开发套件二周目评测:简单声音采集之驱动MAX4466高精度声音传感器》
cc1989summer 发表于 2024-10-29 00:31
楼主,STM32H7S78-DK本身就带数字麦克风,理论上可以通过该麦克风实现音量检测……
我开始也是看他有数字麦克风,才想测评弄这个,但是我只找到一个麦克风接口,有点疑惑。所以就干脆直接买了一个麦克风
- 2024-10-21
-
回复了主题帖:
《STM32H7S78-DK 开发套件二周目评测:简单声音采集之频率检测与显示》
Jacktang 发表于 2024-10-20 08:50
这个对检测的音频信号有没有要求呢,比如最低的频率大小
按照目前的采样率的话,理论上的最低频率大约为1hz,最高大约是500hz的样子
- 2024-10-18
-
发表了日志:
《STM32H7S78-DK 开发套件二周目评测:简单声音采集之频率检测与显示》
-
发表了主题帖:
《STM32H7S78-DK 开发套件二周目评测:简单声音采集之频率检测与显示》
在实现adc基础上,实现对音频信号频率的检测。读取模拟信号并通过 FFT(快速傅里叶变换)分析频率成分。同时,在串口输出检测到的频率,并通过 LED 显示结果。
一、硬件连接
引脚连接:
将 MAX4466 传感器的输出引脚连接到 STM32 的 ADC 输入引脚(PC0,即 ADC1_IN10)。
LED选取PO5。在高电平时候点亮。
二、使用 CubeMX 进行配置
在 CubeMX 中进行如下设置:
2.1 ADC 模块配置
时钟设置:配置 ADC 时钟为 84MHz,选择 ADC 的时钟分频器为 1,以达到较高的采样精度。
采样时间:设置 ADC 的采样时间为 15.5 个 ADC 时钟周期,以提高采样精度。
通道设置:将 ADC 通道设置为 ADC1_IN10(PC0),确保能够读取传感器输出。
2.2 TIM 模块配置
定时器配置:配置 TIM2 为定时器,用于生成定时中断。
设置预分频器为 7999,自动重装载值为 999,以设定频率采样率(例如 1kHz)。
2.3 GPIO 设置
将 PO5 配置为推挽输出模式,用于控制 LED。
生成代码并导入 Keil,在 CubeMX 中生成代码,并导入 Keil 进行后续的代码开发。
四、代码实现
4.1 初始化模块
在 Keil 中,初始化 ADC、定时器和 LED 控制的代码如下:
#include "arm_math.h"
#include "stm32h7xx_hal.h"
#define FFT_SIZE 1024 // FFT 输入数组的大小
float32_t input[FFT_SIZE];
float32_t output[FFT_SIZE];
arm_cfft_instance_f32 fft_instance;
TIM_HandleTypeDef htim2;
ADC_HandleTypeDef hadc1;
void ADC_Init(void) {
ADC_ChannelConfTypeDef sConfig = {0};
__HAL_RCC_ADC12_CLK_ENABLE(); // 使能 ADC 时钟
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV2;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
HAL_ADC_Init(&hadc1);
sConfig.Channel = ADC_CHANNEL_10; // 选择通道 10
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES; // 设置采样时间
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}
uint32_t Read_ADC_Value(void) {
uint32_t adc_value = 0;
HAL_ADC_Start(&hadc1); // 启动 ADC
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); // 等待转换完成
adc_value = HAL_ADC_GetValue(&hadc1); // 获取 ADC 值
HAL_ADC_Stop(&hadc1); // 停止 ADC
return adc_value;
}
void TIM_Init(void) {
__HAL_RCC_TIM2_CLK_ENABLE(); // 使能 TIM2 时钟
htim2.Instance = TIM2;
htim2.Init.Prescaler = 7999; // 对应 1kHz
htim2.Init.Period = 999; // 对应 1ms
htim2.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
HAL_TIM_Base_Init(&htim2); // 初始化定时器
HAL_TIM_Base_Start_IT(&htim2); // 启动定时器中断
}
void FFT_Init(void) {
arm_cfft_init_f32(&fft_instance, FFT_SIZE); // 初始化 FFT 实例
}
4.2 FFT 处理
使用 CMSIS DSP 库实现 FFT 分析信号频率:
void Perform_FFT(void) {
arm_cfft_f32(&fft_instance, input, 0, 1); // 执行 FFT
arm_cmplx_mag_f32(input, output, FFT_SIZE); // 计算幅度
}
4.3 频率检测与 LED 显示
计算频率并控制 LED 的开关状态的代码如下:
void Display_Frequency(void) {
float max_value = output[0];
uint32_t max_index = 0;
for (uint32_t i = 1; i < FFT_SIZE / 2; i++) {
if (output[i] > max_value) {
max_value = output[i];
max_index = i;
}
}
// 控制 LED 的开关状态
if (max_index > 10) {
HAL_GPIO_WritePin(GPIOO, GPIO_PIN_5, GPIO_PIN_SET); // 点亮 LED
} else {
HAL_GPIO_WritePin(GPIOO, GPIO_PIN_5, GPIO_PIN_RESET); // 熄灭 LED
}
}
4.4 主程序
在主循环中进行 ADC 采集、FFT 处理和 LED 控制的代码如下:
int main(void) {
HAL_Init();
SystemClock_Config();
ADC_Init();
TIM_Init();
FFT_Init();
while (1) {
UART_Send_Data(corrected_value);
}
}
// 定时器溢出回调
void HAL_TIM_PERIOD_ELAPSED_CALLBACK(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
// 每次定时器溢出时读取 ADC 值
uint32_t adc_value = Read_ADC_Value(); // 获取 ADC 值
static uint32_t sample_index = 0;
input[sample_index] = (float32_t)adc_value; // 存储到 FFT 输入数组
sample_index++;
// 如果达到 FFT_SIZE 大小,则进行 FFT 计算
if (sample_index >= FFT_SIZE) {
sample_index = 0; // 重置索引
Perform_FFT();
Display_Frequency();
}
}
}
-
发表了日志:
《STM32H7S78-DK 开发套件二周目评测:简单声音采集之驱动MAX4466高精度声音传感器》
-
发表了主题帖:
《STM32H7S78-DK 开发套件二周目评测:简单声音采集之驱动MAX4466高精度声音传感器》
本帖最后由 ccccccc@ 于 2024-10-18 11:52 编辑
MAX4466是一款高精度声音传感器,能够将声音信号转化为相应的模拟电压输出。STM32H7系列微控制器具备高性能的ADC(模数转换器)功能,可以精确地采集模拟信号并进行数字化处理。
使用STM32H7S78-DK开发板,通过ADC(模数转换器)采集MAX4466高精度声音传感器的模拟信号,最终实现一个简单的音量检测功能。
有下面个步骤:
- 从MAX4466传感器采集音频信号。
- 对采集的数据进行处理,获得稳定的音量信息。
硬件连接:
引脚连接:
如上图所示,可以将MAX4466传感器的输出引脚连接到STM32的ADC输入引脚(即PC0,ADC12_IN10),设置该GPIO的ADC功能属性。
使用CubeMX进行配置:
在CubeMX中,进行如下设置:
选择ADC模块:选择ADC1,设置为12位分辨率,适合音频信号处理。
设置采样时间:设置为3.5个ADC时钟周期,确保稳定采样。
设置转换模式:选择单次转换模式(Single Conversion),适合简单应用。
时钟设置: 确保ADC时钟频率与采样时间相匹配,避免采样延迟。
引脚设置:配置PC0为ADC输入模式。
生成代码并导入Keil:
在CubeMX中生成代码,并导入Keil进行后续的代码开发。
在Keil中,检查 初始化ADC并读取值的代码
#include "stm32h7xx_hal.h"
ADC_HandleTypeDef hadc1;
void ADC_Init(void) {
// 使能ADC时钟
__HAL_RCC_ADC12_CLK_ENABLE();
// ADC配置
hadc1.Instance = ADC1;
hadc1.Init.Resolution = ADC_RESOLUTION_12B; // 12位分辨率
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; // 禁用扫描模式
hadc1.Init.ContinuousConvMode = DISABLE; // 禁用连续转换
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 数据右对齐
hadc1.Init.NbrOfConversion = 1; // 单次转换
HAL_ADC_Init(&hadc1); // 初始化ADC
}
uint32_t Read_ADC_Value(void) {
HAL_ADC_Start(&hadc1); // 启动ADC转换
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); // 等待转换完成
return HAL_ADC_GetValue(&hadc1); // 返回ADC值
}
数据处理:在main代码中进行添加下面的代码进行采集信息的数据处理
滤波处理: 使用简单的移动平均滤波器平滑采样值,减少波动。
float moving_average(float new_value) {
static float avg = 0;
static int count = 0;
count++;
avg += (new_value - avg) / count; // 计算平均值
return avg;
}
偏移量校正:在初始化时获取基准值,以确保测量的准确性。
static int32_t offset = 0;
void Calculate_Offset(void) {
for (int i = 0; i < 100; i++) {
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
offset += HAL_ADC_GetValue(&hadc1); // 累加ADC值
}
offset /= 100; // 计算平均偏移量
}
uint32_t Get_Corrected_ADC_Value(void) {
uint32_t adc_value = Read_ADC_Value(); // 读取ADC值
return adc_value - offset; // 返回校正后的值
}
UART配置:
通过UART输出采样结果,方便观察和调试。当然也可以使用LED进行对应的显示。在CUBEMAX进行对应的配置;
UART_HandleTypeDef huart1;
void UART_Init(void) {
// 使能USART1时钟
__HAL_RCC_USART1_CLK_ENABLE();
// USART配置
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200; // 波特率
huart1.Init.WordLength = USART_WORDLENGTH_8B; // 字长
huart1.Init.StopBits = USART_STOPBITS_1; // 停止位
huart1.Init.Parity = USART_PARITY_NONE; // 无奇偶校验
huart1.Init.Mode = USART_MODE_TX_RX; // 发送接收模式
HAL_UART_Init(&huart1); // 初始化UART
}
void UART_Send_Data(uint32_t data) {
char buffer[20];
sprintf(buffer, "ADC Value: %lu\n", data); // 格式化输出
HAL_UART_Transmit(&huart1, (uint8_t*)buffer, strlen(buffer), HAL_MAX_DELAY); // 发送数据
}
7.主程序:
在主循环中读取ADC值,并通过UART输出。
int main(void) {
HAL_Init(); // 初始化HAL库
ADC_Init(); // 初始化ADC
UART_Init(); // 初始化UART
Calculate_Offset(); // 计算偏移量
while (1) {
uint32_t corrected_value = Get_Corrected_ADC_Value(); // 获取校正后的ADC值
float smoothed_value = moving_average(corrected_value); // 平滑处理
UART_Send_Data(corrected_value); // 发送ADC值
}
}
8.关于噪声处理:
滤波策略:可以通过选择适当的滤波窗口,达到避免数据波动的效果。并且考虑使用中位数滤波或加权平均滤波,以处理异常值和噪声。
9.当然还可以进一步进行性能优化:
使用DMA: 对于快速连续采样的需求,可以考虑使用DMA(直接存储器访问)进行数据采集,减轻CPU负担。
DMA_HandleTypeDef hdma_adc1;
void DMA_Init(void) {
__HAL_RCC_DMA2_CLK_ENABLE(); // 使能DMA时钟
hdma_adc1.Instance = DMA2_Stream0;
hdma_adc1.Init.Channel = DMA_CHANNEL_0; // 选择通道
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; // 从外设到内存
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; // 禁用外设地址递增
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; // 启用内存地址递增
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; // 外设数据对齐
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; // 内存数据对齐
hdma_adc1.Init.Mode = DMA_CIRCULAR; // 循环模式
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH; // 高优先级
HAL_DMA_Init(&hdma_adc1); // 初始化DMA
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1); // 连接DMA与ADC
}
- 2024-10-10
-
回复了主题帖:
《STM32H7S78-DK 开发套件一周目评测:初试点灯,问题重重》
Jacktang 发表于 2024-10-10 07:26
右键设置appli为活动项,进行编译,但是使用mdk直接进行烧录时候,开发板没有反应,这个是不是编译环境那里 ...
编译出来的文件直接烧录又没有问题,可以正常运行。而且mdk点击烧录时候,他又显示烧录成功。
-
发表了主题帖:
《STM32H7S78-DK 开发套件一周目评测:初试点灯,问题重重》
距离上一次开箱又过了很久,终于抽出时间继续埋头码字。
看其他的帖子,开机后都能进行触屏历程,我的上电后却没有反应。查看活动告知,可能是上一个使用者把历程给刷掉了,问题不大。
我来到官方支持下载了两个例程文件,分别是:
上面一个是hex文件,下面一个是工程文件,这里需要求助一下,hex文件进行烧录时候会显示烧录失败
下面的工程使用MDK-arm进行编译时候会报错缺一堆库
我查看路径时候,发现有些文件,在路径中找不到:
感觉处处碰壁,索性开始尝试点灯
使用cubemax直接对对应的四个引脚进行初始化:
然后对工程进行配置,我使用mdk进行开发:
生成代码后,点开mdk文件:
里面有两个文件分别是boot和appli,对boot进行编译时候也有报错,但是我们目前只需要烧录appli
右键设置appli为活动项,进行编译,但是使用mdk直接进行烧录时候,开发板没有反应,我都在想是不是遇到一个坏掉的,再使用programe进行烧录,就成功了;
目前几个问题都还不知道原因,只是验证了开发环境和步骤没有什么问题,接下来开始进行adc的开发。。。
- 2024-09-19
-
发表了主题帖:
《STM32H7S78-DK 开发套件一周目评测:初识与准备》
一、引言
本次评测重点聚焦于 STM32H7S78-DK 开发套件的数字麦克风功能。在一周目的评测中,我们将深入熟悉该开发套件的硬件结构与相关文档,精心准备测试设备和工具,并搭建测试环境,从而为后续的深入评测筑牢坚实基础。
二、开箱初体验
当双手触及 STM32H7S78-DK 开发套件的包装时,立刻能感受到其包装的扎实与专业风范。怀着满心的期待,小心翼翼地打开包装,首先映入眼帘的是采用防静电袋精心包装好的开发板、STMod + 扩展板、WiFi 模组以及一张内存卡。
开发板的外观设计简洁大方,电路板上的元件布局合理,尽显工艺之精良。一眼望去,STM32H7S78 微控制器以及丰富的通信接口清晰可见。开发板的边缘处理得极为光滑,毫无任何毛刺,充分彰显了高品质的制造工艺。
三、STM32H7S78-DK 开发套件硬件介绍
STM32H7S78-DK 开发套件为开发者倾力打造了强大的硬件平台,并赋予了丰富多样的功能。该探索套件涵盖了一整套硬件功能,能够助力用户对众多外设进行评估,诸如 USB Type-C、Octo-SPI 闪存与 Hexadeca-SPI PSRAM 器件、音频编解码器、数字麦克风、ADC、灵活扩展连接器以及用户按钮等。四个灵活的扩展连接器为特定应用(例如无线连接、模拟应用和传感器)提供了轻松且无限制的扩展功能。
其中,STM32H7S7L8H6H 微控制器具备三个 I²C 总线、六个 SPI 端口、三个 USART 端口、两个 SDMMC 端口、两个 CAN 端口、一个以太网端口、两个 SAI 端口、两个 12 位 ADC、一个嵌入式降压转换器、两个 Octo-SPI 存储器接口、一个 Hexadeca-SPI 接口、带供电功能的 USB OTG HS 端口、LCD-TFT 控制器、灵活存储控制器(FMC)、8 至 14 位 DCMI 接口、JTAG 和 SWD 调试支持。STM32H7S78-DK 探索套件还集成了用于 STM32 MCU 的 STLINK-V3EC 嵌入式在线调试器和编程器,并且带有 USB 虚拟 COM 端口桥接器,同时配备了全面的 MCU 软件包。
四、文档介绍
STM32H7S78-DK 开发套件提供了极为详细的文档,其中包括用户手册、参考手册、数据手册等。这些文档对于深入了解开发套件的硬件结构、功能特性以及开发方法有着极大的帮助。可以前往以下网址进行对应资料的下载:
数据摘要以及硬件电路图:https://www.st.com.cn/zh/evaluation-tools/stm32h7s78-dk.html#cad-resources;
keil 开发库:https://www.keil.arm.com/packs/stm32h7rsxx_dfp-keil/devices/;
官方 demo:https://www.st.com/resource/en/demos_resources/stm32h7s78-dk-touchgfx-demo-1-1-2.zip;
更多参考资料:https://www.st.com.cn/zh/microcontrollers-microprocessors/stm32h7s7l8.html#documentation。
五、开发环境的准备
安装开发工具
我个人比较习惯使用 keil 进行开发,不过 STM32CubeIDE 的快速上手特性也着实令人着迷。由于本次测试时间周期较短,再加上工作较为繁忙,因此比较倾向于使用 Cube 来完成本次测试。
STM32CubeIDE:这是一款基于 Eclipse 的集成开发环境,全力支持 STM32 系列微控制器的开发。可以从 ST 官网下载安装:https://www.st.com.cn/zh/development-tools/stm32cubeide.html。
Keil MDK:另一种常用的开发工具,同样支持 STM32 系列微控制器的开发。可从 Keil 官网下载:https://www.keil.com/download/product/。
安装驱动程序
开发套件可能需要安装特定的驱动程序,以确保与计算机能够正常通信。本开发板支持 STLINK-V3EC 嵌入式在线调试器和编程器。可以在 ST 官网或开发套件的文档中找到相应的驱动程序下载链接:https://www.st.com.cn/zh/development-tools/stsw-link007.html。
- 2024-09-12
-
回复了主题帖:
读书活动颁奖:《Rust实战》
下次换个不用互动的活动参加,退回邮费自理感觉有点被伤到。。。
- 2024-09-10
-
回复了主题帖:
测评入围名单:STM32H7R/S 高性能MCU 开启全新的创新机遇!纵享ST 2024高性能新品!
个人信息无误,确认可以完成评测计划
- 2024-07-12
-
回复了主题帖:
《Rust实战》书友互动剧终:信号、中断和异常
yangjiaxu 发表于 2024-7-4 16:42
rust的中断都包含了什么?是CPU有紧急事情处理会产生中断?还是程序崩溃会有中断呢?
Rust 中的中断通常指硬件中断,由外部设备产生,用于处理突发事件。涉及中断向量表、处理程序、优先级与嵌套、上下文保存和恢复等。Rust 本身无直接中断支持,需通过操作系统或硬件接口实现。程序崩溃是否产生中断取决于具体情况,有时会触发相应中断处理机制
- 2024-07-01
-
回复了主题帖:
《Rust实战》书友互动第十一集:内核
如今,Rust 内核编程在嵌入式领域备受关注。其强大的内存安全和并发性看似对嵌入式 MCU 颇具适用性,但在实际应用中,移植现有代码存在诸多难点,比如现有的 C 或 C++ 代码库由于语言特性的差异,在移植到 Rust 时,数据类型转换、指针操作以及内存管理方式的改变都可能带来大量的修改工作。而且获取足够支持也面临挑战,例如部分主流的嵌入式 MCU 厂商可能尚未提供完善的开发文档、工具以及技术支持,Rust 社区对于某些特定的 MCU 架构和新出的芯片型号,可能在硬件抽象层和驱动库的更新速度上无法及时跟上硬件发展的步伐。
就我个人而言,觉得 Rust 的严格安全性和高效性理论上很适合资源受限的嵌入式环境。不过,它复杂的语法和全新的编程思维难免增加学习与开发成本,而且当前其生态对特定 MCU 的支持或许还不够完善。这也引出了一系列问题,像 Rust 的编译结果在不同架构的嵌入式 MCU 上性能差异究竟多大?社区提供的库能否满足特定硬件功能需求?怎样才能降低新开发者将 Rust 应用于嵌入式 MCU 的门槛?
- 2024-06-18
-
回复了主题帖:
《Rust实战》书友互动第九集:时间与时间保持
nemon 发表于 2024-6-11 15:09
如果想实现类似c语言中#if来导入库的功能,用什么形式?
顺便问个偏门问题吧——哪家NTP服务 ...
在 Rust 中没有直接类似 C 语言中 #if 来导入库的方式。Rust 的依赖管理是通过 Cargo.toml 文件来进行配置的。至于哪家 NTP 服务器时间不准,这也不确定,不同的 NTP 服务器可能会因为各种原因出现短暂的时间偏差,一般来说知名的、广泛使用的 NTP 服务器可靠性会高一些。
- 2024-06-10
-
发表了主题帖:
《rust实战》简单的网络通信——TCP、UDP通讯的实现
本帖最后由 ccccccc@ 于 2024-6-10 00:35 编辑
网络通信是现代应用程序的重要组成部分。本文将介绍如何使用Rust实现简单的TCP和UDP通讯。
实现TCP服务器和客户端
首先,我们实现一个简单的TCP服务器,它可以接收客户端的连接并回显收到的数据。
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
use std::thread;
fn handle_client(mut stream: TcpStream) {
let mut buffer = [0; 512];
while match stream.read(&mut buffer) {
Ok(size) => {
stream.write(&buffer[0..size]).unwrap();
true
},
Err(_) => {
println!("发生错误,终止连接");
stream.shutdown(std::net::Shutdown::Both).unwrap();
false
}
} {}
}
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
println!("服务器已启动,等待连接...");
for stream in listener.incoming() {
match stream {
Ok(stream) => {
thread::spawn(|| handle_client(stream));
},
Err(e) => {
println!("错误: {}", e);
}
}
}
}
然后,实现TCP客户端,连接到服务器并发送和接收数据。
use std::net::TcpStream;
use std::io::{Read, Write};
fn main() {
match TcpStream::connect("127.0.0.1:7878") {
Ok(mut stream) => {
let msg = b"你好,世界!";
stream.write(msg).unwrap();
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
println!("接收到: {}", String::from_utf8_lossy(&buffer));
},
Err(e) => {
println!("无法连接到服务器: {}", e);
}
}
}
测试TCP通信
为了验证TCP服务器和客户端的功能,我们编写了以下测试用例。
#[cfg(test)]
mod tests {
use super::*;
use std::net::TcpStream;
use std::thread;
use std::time::Duration;
#[test]
fn test_tcp_communication() {
thread::spawn(|| {
main();
});
thread::sleep(Duration::from_secs(1));
let mut stream = TcpStream::connect("127.0.0.1:7878").unwrap();
let msg = b"你好,世界!";
stream.write(msg).unwrap();
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
assert_eq!(&buffer[0..msg.len()], msg);
}
}
实现UDP通信
我们实现一个简单的UDP客户端和服务器,发送和接收数据。
用户端:
use std::net::UdpSocket;
fn main() {
let socket = UdpSocket::bind("127.0.0.1:34254").expect("无法绑定地址");
socket.connect("127.0.0.1:4242").expect("无法连接到服务器");
socket.send(b"你好,世界!").expect("发送数据失败");
let mut buf = [0; 512];
let (amt, src) = socket.recv_from(&mut buf).expect("接收数据失败");
println!("从 {} 接收到: {}", src, String::from_utf8_lossy(&buf[..amt]));
}
服务端:
use std::net::UdpSocket;
fn main() {
// 创建一个UDP套接字,并绑定到本地地址
let socket = UdpSocket::bind("127.0.0.1:4242").expect("无法绑定地址");
let mut buf = [0; 512];
loop {
// 接收来自客户端的消息
let (amt, src) = socket.recv_from(&mut buf).expect("接收数据失败");
// 打印接收到的数据和数据来源地址
println!("从 {} 接收到: {}", src, String::from_utf8_lossy(&buf[..amt]));
// 响应客户端的消息
socket.send_to(b"Hello, Client!", &src).expect("发送数据失败");
}
}
测试UDP通信
为了验证UDP通信的功能,为UDP服务器编写测试代码:
#[cfg(test)]
mod tests {
use super::*;
use std::net::UdpSocket;
#[test]
fn test_udp_server() {
// 创建一个UDP客户端
let client_socket = UdpSocket::bind("127.0.0.1:0").expect("无法绑定客户端地址");
// 启动UDP服务器
std::thread::spawn(|| {
super::main();
});
// 发送消息给服务器
client_socket.send_to(b"你好,服务器!", "127.0.0.1:4242").expect("发送数据失败");
// 接收来自服务器的响应消息
let mut buf = [0; 512];
let (amt, _) = client_socket.recv_from(&mut buf).expect("接收数据失败");
// 验证服务器的响应消息
assert_eq!(String::from_utf8_lossy(&buf[..amt]), "Hello, Client!");
}
}
然后,我们为UDP服务器编写测试代码:
#[cfg(test)]
mod tests {
use super::*;
use std::net::UdpSocket;
#[test]
fn test_udp_server() {
// 创建一个UDP客户端
let client_socket = UdpSocket::bind("127.0.0.1:0").expect("无法绑定客户端地址");
// 启动UDP服务器
std::thread::spawn(|| {
super::main();
});
// 发送消息给服务器
client_socket.send_to(b"你好,服务器!", "127.0.0.1:4242").expect("发送数据失败");
// 接收来自服务器的响应消息
let mut buf = [0; 512];
let (amt, _) = client_socket.recv_from(&mut buf).expect("接收数据失败");
// 验证服务器的响应消息
assert_eq!(String::from_utf8_lossy(&buf[..amt]), "Hello, Client!");
}
}
易错点和注意事项
端口绑定冲突:确保服务器和客户端绑定的端口不冲突。
数据格式匹配:确保发送和接收的数据格式一致。
网络延迟处理:在测试中适当增加延迟以确保服务器启动完成。