一、系统逻辑图并分析
上半部分主要是描述了数据的流通导向,其中值得关注的点是采样频率为什么是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没有那么容易,如果不是跑特别大的工程,那裸机还是相当吃香的。