基于STM32的HAL库+FreeRTOS+DMA实现简单的语音存储与回放

一、系统逻辑图并分析

        上半部分主要是描述了数据的流通导向,其中值得关注的点是采样频率为什么是16khz,因为人的声音频率为0~3400hz,再根据奈奎斯特采样定率(如果要把采集到的信号还原为原来的样子且不失真的话,采样频率至少为采样信号的频率的两倍),本来8khz也足够,但是高一倍也无伤大雅,而且可以提高一些音质,就定为16khz了。下半部分的显示和计时主要是人机交互部分,更方便观察,至于LED的闪烁任务只是我用来观察烧录后运行时是否会出现死机的现象,如果出现数组长度不够数据溢出或者队列未初始化等都会直接卡住,也算是给机器一个呼吸吧,控制方面较为简单,按键和蓝牙(串口)进入相应的中断回调函数,然后写完队列后并退出即可,最后在控制任务中来进行处理,中断里面不能执行耗时很长的代码,不然后果很严重,必须快进快出,所以使用队列的方式还是挺不错的。

二、关键代码呈现

1.ADC_DAC.h

/*--------------------------------- 头文件防护宏 ---------------------------------*/
#ifndef _ADC_DAC_H_
#define _ADC_DAC_H_



/*--------------------------------- 系统头文件包含 --------------------------------*/
#include "main.h"            // 主工程头文件(外设初始化定义)
#include "cmsis_os.h"        // CMSIS-RTOS API(FreeRTOS集成)
#include <stdatomic.h>       // C11原子操作支持

/*--------------------------------- 类型简化定义 --------------------------------*/
#define u8  uint8_t          // 无符号8位类型简写
#define u32 uint32_t         // 无符号32位类型简写

/*--------------------------------- 硬件参数宏定义 --------------------------------*/
#define NUM_BUFFERS         2  // 双缓冲
#define ADC_SAMPLES        128       // ADC采样缓冲区元素个数(16位数组,总大小256字节)
#define BUFFER_SIZE         256
#define SAMPLE_RATE        16000     // 采样率16kHz
#define FLASH_TOTAL_SIZE   2097152   // W25Q16 Flash总容量(2MB = 2^21字节)


typedef struct {
    uint8_t data[ADC_SAMPLES/2];
    uint32_t address;
  } FlashWriteRequest;

/*--------------------------------- FreeRTOS队列句柄外部声明 ------------------------*/
extern QueueHandle_t xTIMQueue;       // 计时值传输队列(uint8_t类型)
extern QueueHandle_t xCommandQueue;    // 命令队列(uint8_t类型,存储按键/蓝牙指令)
extern QueueHandle_t xFlashQueue;      // Flash写入请求队列(FlashWriteRequest结构体)
extern QueueHandle_t xDACQueue;  // 用于传递准备好的缓冲索引
/*--------------------------------- FreeRTOS任务句柄外部声明 ------------------------*/
extern TaskHandle_t xADCTaskHandle;    // ADC任务句柄(当前可能未启用)
extern TaskHandle_t xDACTaskHandle;    // DAC任务句柄(音频播放控制)
extern TaskHandle_t xFlashReadTaskHandle; // Flash读取任务句柄

/*--------------------------------- 全局变量外部声明 ------------------------------*/
extern uint8_t tim_num;                // 计时计数器(每秒递增,用于显示)
extern volatile uint16_t adc_buffer[ADC_SAMPLES];  // ADC采样缓冲区(需4字节对齐)
extern uint16_t dac_buf[NUM_BUFFERS][BUFFER_SIZE];  // 双缓冲
extern volatile uint8_t active_buf;            // 当前DMA传输的缓冲区
extern volatile uint32_t flash_write_addr;         // Flash写入地址指针
extern volatile uint32_t read_addr;             // Flash读取地址
extern atomic_int recording_flag;      // 原子操作录音标志(0-空闲,1-录音中)
extern atomic_int playing_flag;        // 原子操作播放标志(0-空闲,1-播放中)
extern int monitor_flag;

/*--------------------------------- 函数声明区域 --------------------------------*/
/* LED控制任务(优先级1) */
void vTaskLED(void * pvParameters);

/* OLED刷新任务(优先级1) */
void vOLEDrefresh(void * pvParameters);

/* Flash写入任务(优先级2) */
void FlashWriteTask(void *pvParameters);

/* 系统控制核心任务(优先级3) */
void ControlTask(void *pvParameters);


void FlashReadTask(void *pvParameters);


#endif /* _ADC_DAC_H_ */

2.ADC_DAC.c

/*--------------------------------- 头文件包含 ---------------------------------*/
#include "adc_dac.h"         // ADC/DAC模块驱动头文件
#include "queue.h"           // FreeRTOS队列操作头文件
#include "string.h"          // 标准字符串操作头文件
#include "dma.h"             // DMA控制器配置头文件

/*------------------------------- 全局变量声明 --------------------------------*/
atomic_int recording_flag = 0;   // 原子操作录音标志(0-空闲,1-录音中)
atomic_int playing_flag = 0;     // 原子操作播放标志(0-空闲,1-播放中)

/*--------------------------- 外设句柄外部声明 ----------------------------*/
extern TIM_HandleTypeDef htim2,htim6,htim4;  // 定时器句柄(TIM2-ADC触发,TIM6-DAC触发,TIM4-计时)
extern SPI_HandleTypeDef hspi3;              // SPI3句柄(连接Flash芯片)
extern ADC_HandleTypeDef hadc1;              // ADC1句柄(音频采集)
extern DAC_HandleTypeDef hdac;               // DAC句柄(音频播放)
extern UART_HandleTypeDef huart1;            // 串口1句柄(蓝牙通信)
extern DMA_HandleTypeDef hdma_adc1;          // ADC DMA句柄
extern DMA_HandleTypeDef hdma_dac1;

/*------------------------------ 队列句柄声明 ------------------------------*/
QueueHandle_t xTIMQueue;       // 计时值传输队列(uint8_t类型)

/*-------------------------- 外部函数声明(Flash/OLED) --------------------------*/
extern uint8_t W25Q16_Write(uint8_t* pData, uint32_t WriteAddr, uint32_t Size);  // Flash写操作
extern uint8_t W25Q16_Read(uint8_t* pData, uint32_t ReadAddr, uint32_t Size);    // Flash读操作
extern uint8_t W25Q16_Erase_All(void);        // Flash全片擦除
extern void OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size1,u8 mode);  // OLED显示数字
extern void OLED_Refresh(void);               // OLED刷新屏幕

/*-------------------------------- 全局变量定义 --------------------------------*/
uint8_t tim_num=0;                // 计时计数器(每秒递增)
extern uint8_t aRxBuffer;         // 串口接收缓冲区(蓝牙指令存储)
volatile uint16_t adc_buffer[ADC_SAMPLES];  // ADC采样缓冲区(16位数组,需4字节对齐)
uint16_t dac_buf[NUM_BUFFERS][BUFFER_SIZE];  // 双缓冲
volatile uint8_t active_buf = 0;            // 当前DMA传输的缓冲区
volatile uint32_t flash_write_addr = 0;     // Flash写入地址指针
volatile uint32_t read_addr = 0;      // Flash读取地址
int monitor_flag=0;

/*---------------------------- FreeRTOS队列/任务句柄 ----------------------------*/
QueueHandle_t xCommandQueue;      // 命令队列(uint8_t类型,存储按键/蓝牙指令)
QueueHandle_t xFlashQueue;        // Flash写入队列(FlashWriteRequest结构体)
QueueHandle_t xDACQueue;  // 用于传递准备好的缓冲索引
TaskHandle_t xADCTaskHandle, xDACTaskHandle;  // ADC/DAC任务句柄
TaskHandle_t xFlashReadTaskHandle; // Flash读取任务句柄

/*-------------------------- TIM4周期中断回调函数 --------------------------*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
  if (htim->Instance == TIM4) {   // 仅处理TIM4中断
      BaseType_t xHigherPriorityTaskWoken = pdFALSE;  // 任务切换标志
      tim_num++;                  // 计时值递增(每秒一次)

      /* 根据录音/播放状态处理计时 */
      if (atomic_load(&recording_flag)) {   // 录音模式
          // 将计时值覆盖写入队列(队列长度1,保留最新值)
          xQueueOverwriteFromISR(xTIMQueue, &tim_num, &xHigherPriorityTaskWoken);
      } else if (atomic_load(&playing_flag)) {  // 播放模式
        xQueueOverwriteFromISR(xTIMQueue, &tim_num, &xHigherPriorityTaskWoken);  
        // 可在此添加播放计时逻辑
      }
      
      // 触发任务切换(若需要)
      portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
  }
}

/*------------------------------- LED控制任务 -------------------------------*/
void vTaskLED(void * pvParameters) {
  while(1) {
    LED_OFF;                     // 关闭LED
    vTaskDelay(pdMS_TO_TICKS(500));  // 延时500ms(FreeRTOS节拍转换)
    LED_ON;                      // 开启LED
    vTaskDelay(pdMS_TO_TICKS(500)); // 再延时500ms
  }
}

/*----------------------------- OLED刷新任务 -----------------------------*/
void vOLEDrefresh(void * pvParameters) {
    uint8_t tim_num_output;      // 接收计时值的临时变量
    while (1) {
        // 阻塞式接收队列数据(无限等待)
        if (xQueueReceive(xTIMQueue, &tim_num_output, portMAX_DELAY) == pdPASS) {
            OLED_ShowNum(40, 20, tim_num_output, 3, 16, 1);  // 在(40,20)显示3位数字
            OLED_Refresh();      // 刷新屏幕(需确保非阻塞)
        }
        vTaskDelay(pdMS_TO_TICKS(10));  // 短延时防止任务饥饿
    }
}

/*------------------------- 串口接收完成回调函数(蓝牙) -------------------------*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {  // 仅处理USART1中断
        BaseType_t xHigherPriorityTaskWoken = pdFALSE;
        uint8_t bluetooth_order = aRxBuffer; // 获取接收到的指令

        /* 发送响应"ok"(可选) */
        uint8_t ok[] = {'o','k'};
        HAL_UART_Transmit_IT(&huart1, ok, sizeof(ok));

        /* 发送指令到命令队列 */
        if (xCommandQueue != NULL) {
            xQueueSendFromISR(xCommandQueue, &bluetooth_order, &xHigherPriorityTaskWoken);
        }

        /* 重新使能接收中断(持续监听) */
        HAL_UART_Receive_IT(&huart1, (uint8_t*)&aRxBuffer, 1);

        /* 触发上下文切换 */
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}

/*---------------------------- 按键中断回调函数 ----------------------------*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  uint8_t key = 0xFF;            // 按键值初始化为无效

  /* 消抖处理(50ms间隔) */
  static uint32_t last_tick = 0;
  uint32_t current_tick = HAL_GetTick();
  if (current_tick - last_tick < 50) return;  // 间隔不足50ms视为抖动
  last_tick = current_tick;

  /* 识别具体按键 */
  if (GPIO_Pin == KEY0_Pin) {     // 按键0按下
      key = 1;                   // 编码为1(开始/停止录音)
  } else if (GPIO_Pin == KEY1_Pin) { // 按键1按下
      key = 3;                   // 编码为2(开始播放)
  }

  /* 发送按键指令到命令队列 */
  if (key != 0xFF && xCommandQueue != NULL) {
      xQueueSendFromISR(xCommandQueue, &key, &xHigherPriorityTaskWoken);
  }

  /* 触发任务切换 */
  portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

/*--------------------- ADC DMA半传输完成回调函数 ---------------------*/
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) {
  if(recording_flag) {           // 仅在录音状态下处理
      BaseType_t xHigherPriorityTaskWoken = pdFALSE;
      FlashWriteRequest request; // Flash写入请求结构体

      /* 处理前半缓冲区数据(0-127采样点) */
      uint16_t *pBuf = (uint16_t*)adc_buffer;
      for(int i=0; i<64; i++) {  // 压缩为8位(12→8位)
          request.data[i] = (pBuf[i] >> 4);
      }
      request.address = flash_write_addr;  // 设置写入地址
      xQueueSendFromISR(xFlashQueue, &request, &xHigherPriorityTaskWoken);
      flash_write_addr += 64;     // 地址递增64字节
      portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
  }
}

/*--------------------- ADC DMA全传输完成回调函数 ---------------------*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
  if(recording_flag) {           // 仅在录音状态下处理
      BaseType_t xHigherPriorityTaskWoken = pdFALSE;
      FlashWriteRequest request;

      /* 处理后半缓冲区数据(128-255采样点) */
      uint16_t *pBuf = (uint16_t*)(adc_buffer + 64); // 偏移到后半部分
      for(int i=0; i<64; i++) {
          request.data[i] = (pBuf[i] >> 4);
      }
      request.address = flash_write_addr;
      xQueueSendFromISR(xFlashQueue, &request, &xHigherPriorityTaskWoken);
      flash_write_addr += 64;
      portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
  }
}

/*-------------------------- Flash写入任务 --------------------------*/
void FlashWriteTask(void *pvParameters) {
  FlashWriteRequest request;      // Flash写入请求结构体
  while(1) {
      // 非阻塞接收队列数据(最多等待100ms)
      if(xQueueReceive(xFlashQueue, &request, pdMS_TO_TICKS(100)) == pdPASS) {
          W25Q16_Write(request.data, request.address, 64); // 写入64字节到Flash
      }
      taskYIELD(); // 主动让出CPU,防止低优先级任务饥饿
  }
}

/*--------------------------- 控制任务(核心) ---------------------------*/
void ControlTask(void *pvParameters) {
  uint8_t cmd;                   // 存储接收到的命令
  while (1) {
      // 阻塞式接收命令(无限等待)
      if (xQueueReceive(xCommandQueue, &cmd, portMAX_DELAY)) {
          switch (cmd) {
              case 0x01:  // 开始录音
                  W25Q16_Erase_All();     // 全片擦除Flash(耗时,建议异步)
                  flash_write_addr = 0;   // 复位写入地址
                  atomic_store(&recording_flag, 1);  // 原子操作设置录音标志
                  tim_num = 0;           // 复位计时器

                  HAL_TIM_Base_Start(&htim2);  // 启动TIM2(触发ADC采样)
                  HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_SAMPLES); // 启动ADC DMA
                  HAL_TIM_Base_Start_IT(&htim4);  // 启动TIM4中断(计时)
                  break;

              case 0x02:  // 停止录音
                  atomic_store(&recording_flag, 0); // 清除录音标志

                  /* 彻底停止ADC相关外设 */
                  HAL_ADC_Stop_DMA(&hadc1);        // 停止DMA传输
                  HAL_TIM_Base_Stop(&htim2);       // 停止TIM2触发
                  if (HAL_DMA_GetState(&hdma_adc1) != HAL_DMA_STATE_READY) {
                      HAL_DMA_Abort(&hdma_adc1);    // 强制终止DMA
                  }
                  __HAL_ADC_CLEAR_FLAG(&hadc1, ADC_FLAG_EOC | ADC_FLAG_OVR);  // 清除ADC标志
                  __HAL_DMA_CLEAR_FLAG(&hdma_adc1, DMA_FLAG_TCIF0_4 | DMA_FLAG_HTIF0_4); // 清除DMA标志
                  HAL_TIM_Base_Stop_IT(&htim4);    // 停止TIM4中断
                  __HAL_TIM_CLEAR_FLAG(&htim4, TIM_FLAG_UPDATE);  // 清除TIM4中断标志
                  tim_num = 0;        // 可选:复位计时值
                  break;

                  case 0x03:  // 开始播放
                  atomic_store(&recording_flag, 0); // 确保原子操作停止录音
                  
                  /* 彻底停止ADC相关外设(完全复制0x02的操作) */
                  HAL_ADC_Stop_DMA(&hadc1);        // 停止DMA传输
                  HAL_TIM_Base_Stop(&htim2);       // 停止TIM2触发
                  if (HAL_DMA_GetState(&hdma_adc1) != HAL_DMA_STATE_READY) {
                      HAL_DMA_Abort(&hdma_adc1);    // 强制终止DMA
                  }
                  __HAL_ADC_CLEAR_FLAG(&hadc1, ADC_FLAG_EOC | ADC_FLAG_OVR);  // 清除ADC标志
                  __HAL_DMA_CLEAR_FLAG(&hdma_adc1, DMA_FLAG_TCIF0_4 | DMA_FLAG_HTIF0_4); // 清除DMA标志
                  HAL_TIM_Base_Stop_IT(&htim4);    // 停止TIM4中断
                  __HAL_TIM_CLEAR_FLAG(&htim4, TIM_FLAG_UPDATE);  // 清除TIM4中断标志
              
                  /* 开始播放流程 */
                  atomic_store(&playing_flag, 1);  
                  read_addr = 0;                   // 复位读取地址
                  tim_num = 0; 
                  active_buf = 0;                     
                  HAL_TIM_Base_Start_IT(&htim4);   // 启动TIM4计时(注意:确保与播放无冲突)
                  HAL_TIM_Base_Start(&htim6);       // 启动TIM6触发DAC
                            // 首次启动DMA传输(Normal模式)

                HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, 
                    (uint32_t*)dac_buf[active_buf], 
                    BUFFER_SIZE, 
                    DAC_ALIGN_12B_R);
    
                  break;

              default:    // 未知指令
                  break;
          }
      }
  }
}

/*-------------------------- DMA中断回调处理 --------------------------*/
// 半传输完成回调
void HAL_DAC_ConvHalfCpltCallbackCh1(DAC_HandleTypeDef* hdac){
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 发送后半缓冲区需要填充的通知(0表示前半已传输,需要填充后半)
    uint8_t buf_index = active_buf;
    xQueueSendFromISR(xDACQueue, &buf_index, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 传输完成回调
void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef* hdac){
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 发送前半缓冲区需要填充的通知(切换缓冲区)
    uint8_t prev_buf = active_buf;
    active_buf ^= 1; // 切换缓冲区
    xQueueSendFromISR(xDACQueue, &prev_buf, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

/*-------------------------- Flash读取任务 --------------------------*/
void FlashReadTask(void *pvParameters) {
    uint8_t target_buf;
    uint8_t temp_buffer[BUFFER_SIZE/2]; // 半缓冲区存储

    while (1) {
        if (atomic_load(&playing_flag)) {
            if (xQueueReceive(xDACQueue, &target_buf, portMAX_DELAY) == pdTRUE) {
                // 计算需要填充的缓冲区位置
                uint32_t read_offset = (target_buf == active_buf) ? 
                    BUFFER_SIZE/2 : 0;

                // 从Flash读取数据(每次读取半缓冲区)
                if (W25Q16_Read(temp_buffer, read_addr + read_offset, BUFFER_SIZE/2) != 1) {
                    atomic_store(&playing_flag, 0);
                    continue;
                }

                // 转换并填充到目标缓冲区
                for(int i=0; i<BUFFER_SIZE/2; i++) {
                    if (read_offset == 0) { // 填充前半
                        dac_buf[target_buf][i] = (temp_buffer[i] << 4);
                    } else { // 填充后半
                        dac_buf[target_buf][BUFFER_SIZE/2 + i] = (temp_buffer[i] << 4);
                    }
                }

                // 更新全局地址(当填充后半时更新)
                if (read_offset != 0) {
                    read_addr += BUFFER_SIZE;
                    if (read_addr >= FLASH_TOTAL_SIZE) read_addr = 0;
                }
            }
        } 
        else {
            vTaskDelay(10);
        }
    }
}

3.任务的创建

void MX_FREERTOS_Init(void) {

  TaskHandle_t LEDTask_Hander;   // LED任务句柄(局部变量)
  TaskHandle_t OLEDTask_Hander;  // OLED任务句柄(局部变量)


  xCommandQueue = xQueueCreate(10, sizeof(uint8_t));    // 命令队列(容量10,存储uint8_t指令)
  xDACQueue = xQueueCreate(50, sizeof(uint8_t));
  xTIMQueue = xQueueCreate(1, sizeof(uint8_t));         // 计时队列(容量1,存储最新计时值)
  xFlashQueue = xQueueCreate(50, sizeof(FlashWriteRequest)); // Flash写入队列(容量20,存储写入请求结构体)

  /* 创建系统任务(优先级数字越小优先级越低) */
  xTaskCreate(vTaskLED, "LED", 128, NULL, 1, &LEDTask_Hander);       // LED闪烁任务(优先级1)
  xTaskCreate(vOLEDrefresh, "OLED", 1024, NULL, 1, &OLEDTask_Hander); // OLED刷新任务(堆栈1024字节)y
  xTaskCreate(FlashWriteTask, "Flash", 512, NULL, 4, NULL);          // Flash写入任务(优先级2)
  xTaskCreate(FlashReadTask, "FlashRead", 256, NULL, 5, NULL);       // 中优先级
  xTaskCreate(ControlTask, "Control", 256, NULL, 3, NULL);           // 核心控制任务(优先级3)

  osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
  defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);


}

STM32CubeMX的外设配置就比较简单了,我就不过多赘述了。

三、对于FreeRTOS的感受

        这是第一次用RTOS做东西,相比较域裸机来说还是挺有优势的,比如裸机里面的屏幕刷新还有按键的检测都是采用轮循的方式,如果后面你想要跑点耗时间的程序或者计算量较大如fft那样的,你的按键可能按了3秒才有反应,这时RTOS的优势就显现出来了,其次还有这种按键检测的中断,RTOS用的队列的形式也让我耳目一新,这样就可以实现当没有指令下达的时候,CPU能够将之前用去检测指令的时间用来执行别的程序,这样也可以节约一些资源了,确实很不错,但是相比较于裸机,RTOS的debug没有那么容易,如果不是跑特别大的工程,那裸机还是相当吃香的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值