STM32F407使用Helix库软解MP3并通过DAC输出,最精简的STM32+SD卡实现MP3播放器

news2024/12/25 0:43:37

只用STM32单片机+SD卡+耳机插座,实现播放MP3播放器!

看过很多STM32软解MP3的方案,即不通过类似VS1053之类的解码器芯片,直接用STM32和软件库解码MP3文件,通常使用了labmad或者Helix解码库实现,Helix相对labmad占用的RAM更少。但是大多数参考的方案还是用了外接IIS接口WM98xx之类的音频DAC芯片播放音频,稍显复杂繁琐。STM32F407Vx本身就自带了2路12位DAC输出,最高刷新速度333kHz,除了分辨率差点意思,速度上对于MP3通常44.1kHz采样率来说,用来播放音频绰绰有余了。本文给的方案和源码,直接用STM32软解码MP3并使用自带的2个DAC输出引脚输出音频左右声道。

原理:STM32从SD读取MP3文件原始数据,发送给Helix库解码,Helix解码后输出PCM数据流,将此数据进一步处理转换后,按照左右声道分别存入DAC输出1和2缓存,通过定时器以MP3文件的采样率的频率提供DAC触发节拍,通过DMA取缓存中高12位数据给DAC,在DAC1和2引脚产生音频波形,通过电容耦合到耳机的左右声道上。

MP3源文件是一种经过若干算法,将原始音频数据压缩得来的,软件解码的过程是逆过程,将压缩的音频反向转换为记录了左右声道、幅值的数据流,通常是PCM格式。

PCM:是模拟信号以固定的采样频率转换成数字信号后的表现形式。记录了音频采样的数据,双通道、16bit的PCM数据格式是以0轴为中心,范围为-32768~32767的数值,每个数据占用2字节,左声道和右声道交替存储,如图。

 软解码得到的PCM数据到STM32的DAC缓存需要进一步处理。STM32的DAC是12位的,其输入范围0~4095,而双通道16位的PCM音频数据是左右声道交替存储,且数据范围-32768~32767,因此PCM到STM32的DAC缓存要按照顺序一拆为二,分为左右声道,每个数据再加上32768,使其由short int的范围转换为unsigned short int,即0~65535。由于PCM数据是对音频的采样,因此调节音量(幅值)可以在此步骤一并处理,即音频数据 x 音量 /最大音量。至于DAC是12位,只需将DAC模式设置为左对齐12位,舍弃低4位即可。

到此,STM32的DAC输出引脚上应该已经有音频信号了,通常DAC引脚上串联一个1~10uF的电容用来耦合音频信号,电容越大音质越好,电容另一端接耳机插座的左声道/右声道,插上耳机就可以欣赏音乐啦!音质嘛,反正我是听不出来好不好,跟商品MP3播放器差不多。如果不串联电容,DAC引脚直连耳机插座左右声道也能听到声音,就是有些数字信号噪声也会传进来。如果希望噪声小一些,DAC引脚输出端加一个下图的低通滤波电路也是可以的。

 

  

Helix移植:

Helix源码的官网我没找到,直接用了野火的例程里面的代码,移植也很简单,不用改任何代码,只需要将Helix文件夹拷贝到工程目录里,然后在Keil中添加好文件,以及添加头文件途径,编译即可。工程目录如图。

源码:dac配置

dac.c

/**
  ******************************************************************************
  * @file    dac.c
  * @author  ZL
  * @version V0.0.1
  * @date    September-20-2019
  * @brief   DAC configuration.
  ******************************************************************************
*/
/* Includes ------------------------------------------------------------------*/
#include "dac.h"

/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
#define   CNT_FREQ          84000000      // TIM6 counter clock (prescaled APB1)

/* DHR registers offsets */
#define DHR12R1_OFFSET             ((uint32_t)0x00000008)
#define DHR12R2_OFFSET             ((uint32_t)0x00000014)
#define DHR12RD_OFFSET             ((uint32_t)0x00000020)

/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
uint32_t DAC_DHR12R1_ADDR = (uint32_t)DAC_BASE + DHR12R1_OFFSET + DAC_Align_12b_L;
uint32_t DAC_DHR12R2_ADDR = (uint32_t)DAC_BASE + DHR12R2_OFFSET + DAC_Align_12b_L;

uint16_t DAC_buff[2][DAC_BUF_LEN]; //DAC1、DAC2输出缓冲

/* Private function prototypes -----------------------------------------------*/
static void TIM6_Config(void);

/* Private functions ---------------------------------------------------------*/
/**
  * @brief  DAC初始化
  * @param  none
  * @retval none
*/
void DAC_Config(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	DAC_InitTypeDef  DAC_InitStructure;
	
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
		
	DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;
	DAC_InitStructure.DAC_Trigger = DAC_Trigger_T6_TRGO;
	DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable;
	DAC_Init(DAC_Channel_1, &DAC_InitStructure);
	DAC_Init(DAC_Channel_2, &DAC_InitStructure);
	
	//配置DMA
	DMA_InitTypeDef DMA_InitStruct;
	DMA_StructInit(&DMA_InitStruct);
	
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
	
	DMA_InitStruct.DMA_PeripheralBaseAddr = (u32)DAC_DHR12R1_ADDR;
	DMA_InitStruct.DMA_Memory0BaseAddr = (u32)&DAC_buff[0];//DAC1
	DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToPeripheral;
	DMA_InitStruct.DMA_BufferSize = DAC_BUF_LEN;
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
	DMA_InitStruct.DMA_Priority = DMA_Priority_High;
	DMA_InitStruct.DMA_Channel = DMA_Channel_7;
	DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;
	DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
  DMA_InitStruct.DMA_MemoryBurst   = DMA_MemoryBurst_Single;
  DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
	
	DMA_Init(DMA1_Stream5, &DMA_InitStruct);
		
	DMA_InitStruct.DMA_PeripheralBaseAddr = (u32)DAC_DHR12R2_ADDR;
	DMA_InitStruct.DMA_Memory0BaseAddr = (u32)&DAC_buff[1];//DAC2
	DMA_Init(DMA1_Stream6, &DMA_InitStruct);
		
	//开启DMA传输完成中断
	NVIC_InitTypeDef NVIC_InitStructure;
	
  NVIC_InitStructure.NVIC_IRQChannel = DMA1_Stream6_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
	
	DMA_ClearITPendingBit(DMA1_Stream6, DMA_IT_TCIF6);
	DMA_ClearITPendingBit(DMA1_Stream6, DMA_IT_HTIF6);
	DMA_ITConfig(DMA1_Stream6, DMA_IT_TC, ENABLE);
	DMA_ITConfig(DMA1_Stream6, DMA_IT_HT, ENABLE);

//	DMA_Cmd(DMA1_Stream5, ENABLE);
//	DMA_Cmd(DMA1_Stream6, ENABLE);
	DAC_Cmd(DAC_Channel_1, ENABLE);
  DAC_Cmd(DAC_Channel_2, ENABLE);
	
	DAC_DMACmd(DAC_Channel_1, ENABLE);
	DAC_DMACmd(DAC_Channel_2, ENABLE);
	
	TIM6_Config();
}

//配置DAC采样率和DMA数据长度,并启动DMA DAC
void DAC_DMA_Start(uint32_t freq, uint16_t len)
{
	//设置DMA缓冲长度需要停止DMA
	DAC_DMA_Stop();
	//设置DMA DAC缓冲长度
	DMA_SetCurrDataCounter(DMA1_Stream5, len);
	DMA_SetCurrDataCounter(DMA1_Stream6, len);
	
	//设置定时器
	TIM_SetAutoreload(TIM6, (uint16_t)((CNT_FREQ)/freq));
	
	//启动
	DMA_Cmd(DMA1_Stream5, ENABLE);
	DMA_Cmd(DMA1_Stream6, ENABLE);
}

//停止DMA DAC
void DAC_DMA_Stop(void)
{
	DMA_Cmd(DMA1_Stream5, DISABLE);
	DMA_Cmd(DMA1_Stream6, DISABLE);
}

//定时器6用于设置DAC刷新率
static void TIM6_Config(void)
{
  TIM_TimeBaseInitTypeDef TIM6_TimeBase;

  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
  TIM_TimeBaseStructInit(&TIM6_TimeBase); 
	
  TIM6_TimeBase.TIM_Period        = (uint16_t)((CNT_FREQ)/44100);
  TIM6_TimeBase.TIM_Prescaler     = 0;
  TIM6_TimeBase.TIM_ClockDivision = 0;
  TIM6_TimeBase.TIM_CounterMode   = TIM_CounterMode_Up;
  TIM_TimeBaseInit(TIM6, &TIM6_TimeBase);
	
  TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update);
  TIM_Cmd(TIM6, ENABLE);
}

/**
  * @brief  DAC out1 PA4输出电压
  * @param  dat:dac数值:,0~4095
  * @retval none
*/
void DAC_Out1(uint16_t dat)
{
	DAC_SetChannel1Data(DAC_Align_12b_R,  dat);
	DAC_SoftwareTriggerCmd(DAC_Channel_1, ENABLE);
}

/**
  * @brief  DAC out2 PA5输出电压
  * @param  dat:dac数值:,0~4095
  * @retval none
*/
void DAC_Out2(uint16_t dat)
{
	DAC_SetChannel2Data(DAC_Align_12b_R,  dat);
	DAC_SoftwareTriggerCmd(DAC_Channel_2, ENABLE);
}

/********************************************* *****END OF FILE****/

源码:MP3播放流程 (原创野火,参考了野火的例程,本人进行整理和修改)

MP3player.c

/*
******************************************************************************
* @file    mp3Player.c
* @author  fire
* @version V1.0
* @date    2023-08-13
* @brief   mp3解码
******************************************************************************
*/
#include <stdio.h>
#include <string.h>
#include "ff.h" 
#include "mp3Player.h"
#include "mp3dec.h"
#include "dac.h"
#include "led.h"

/* 推荐使用以下格式mp3文件:
 * 采样率:44100Hz
 * 声  道:2
 * 比特率:320kbps
 */

/* 处理立体声音频数据时,输出缓冲区需要的最大大小为2304*16/8字节(16为PCM数据为16位),
 * 这里我们定义MP3BUFFER_SIZE为2304
 */
#define MP3BUFFER_SIZE  2304
#define INPUTBUF_SIZE   3000

static HMP3Decoder		Mp3Decoder;			/* mp3解码器指针	*/
static MP3FrameInfo		Mp3FrameInfo;		/* mP3帧信息  */
static MP3_TYPE mp3player;            /* mp3播放设备 */
volatile uint8_t Isread = 0;          /* DMA传输完成标志 */
volatile uint8_t dac_ht = 0;          //DAC dma 半传输标志

uint32_t led_delay = 0;

uint8_t inputbuf[INPUTBUF_SIZE]={0};     /* 解码输入缓冲区,1940字节为最大MP3帧大小  */
static short outbuffer[MP3BUFFER_SIZE];  /* 解码输出缓冲区*/

static FIL file;			/* file objects */
static UINT bw;       /* File R/W count */
FRESULT result; 

//从SD卡读取MP3源文件进行解码,并传入DAC缓冲区
int MP3DataDecoder(uint8_t **read_ptr, int *bytes_left)
{
	int err = 0, i = 0, outputSamps = 0;

	//bufflag开始解码 参数:mp3解码结构体、输入流指针、输入流大小、输出流指针、数据格式
	err = MP3Decode(Mp3Decoder, read_ptr, bytes_left, outbuffer, 0);
	
	if (err != ERR_MP3_NONE)	//错误处理
	{
		switch (err)
		{
			case ERR_MP3_INDATA_UNDERFLOW:
							printf("ERR_MP3_INDATA_UNDERFLOW\r\n");
							result = f_read(&file, inputbuf, INPUTBUF_SIZE, &bw);
							*read_ptr = inputbuf;
							*bytes_left = bw;
				break;		
			case ERR_MP3_MAINDATA_UNDERFLOW:
							/* do nothing - next call to decode will provide more mainData */
							printf("ERR_MP3_MAINDATA_UNDERFLOW\r\n");
				break;		
			default:
							printf("UNKNOWN ERROR:%d\r\n", err);		
							// 跳过此帧
							if (*bytes_left > 0)
							{
								(*bytes_left) --;
								read_ptr ++;
							}
				break;
		}
		return 0;
	}
	else		//解码无错误,准备把数据输出到PCM
	{
		MP3GetLastFrameInfo(Mp3Decoder, &Mp3FrameInfo);		//获取解码信息				
		/* 输出到DAC */
		outputSamps = Mp3FrameInfo.outputSamps;						//PCM数据个数
		if (outputSamps > 0)
		{
			if (Mp3FrameInfo.nChans == 1)	//单声道
			{
				//单声道数据需要复制一份到另一个声道
				for (i = outputSamps - 1; i >= 0; i--)
				{
					outbuffer[i * 2] = outbuffer[i];
					outbuffer[i * 2 + 1] = outbuffer[i];
				}
				outputSamps *= 2;
			}//if (Mp3FrameInfo.nChans == 1)	//单声道
		}//if (outputSamps > 0)
					
		//将数据传送至DMA DAC缓冲区
		for (i = 0; i < outputSamps/2; i++)
		{
			if(dac_ht == 1)
			{
				DAC_buff[0][i] = outbuffer[2*i] * mp3player.ucVolume /100 + 32768;
				DAC_buff[1][i] = outbuffer[2*i+1] * mp3player.ucVolume /100 + 32768;
			}
			else
			{
				DAC_buff[0][i+outputSamps/2] = outbuffer[2*i] * mp3player.ucVolume /100 + 32768;
				DAC_buff[1][i+outputSamps/2] = outbuffer[2*i+1] * mp3player.ucVolume /100 + 32768;
			}
		}
		
		return 1;
	}//else 解码正常
}

//读取一段MP3数据,并把读取的指针赋值read_ptr,长度赋值bytes_left
uint8_t read_file(const char *mp3file, uint8_t **read_ptr, int *bytes_left)
{
	result = f_read(&file, inputbuf, INPUTBUF_SIZE, &bw);
	
	if(result != FR_OK)
	{
		printf("读取%s失败 -> %d\r\n", mp3file, result);
		return 0;
	}
	else
	{
		*read_ptr = inputbuf;
		*bytes_left = bw;
		
		return 1;
	}
}

/**
  * @brief  MP3格式音频播放主程序
  * @param  mp3file MP3文件路径
  * @retval 无
  */
void mp3PlayerDemo(const char *mp3file)
{
	uint8_t *read_ptr = inputbuf;
	int	read_offset = 0;				/* 读偏移指针 */
	int	bytes_left = 0;					/* 剩余字节数 */	
	
	mp3player.ucStatus = STA_IDLE;
	mp3player.ucVolume = 15; //音量值,100满
	
	//尝试打开MP3文件
	result = f_open(&file, mp3file, FA_READ);
	if(result != FR_OK)
	{
		printf("Open mp3file :%s fail!!!->%d\r\n", mp3file, result);
		result = f_close (&file);
		return;	/* 停止播放 */
	}
	printf("当前播放文件 -> %s\n", mp3file);
	
	//初始化MP3解码器
	Mp3Decoder = MP3InitDecoder();	
	if(Mp3Decoder == 0)
	{
		printf("初始化helix解码库设备失败!\r\n");
		return;	/* 停止播放 */
	}
	else
	{
		printf("初始化helix解码库完成\r\n");
	}
	
	//尝试读取一段MP3数据,并把读取的指针赋值read_ptr,长度赋值bytes_left
	if(!read_file(mp3file, &read_ptr, &bytes_left))
	{
		MP3FreeDecoder(Mp3Decoder);
		return;	/* 停止播放 */
	}
	
	//尝试解码成功
	if(MP3DataDecoder(&read_ptr, &bytes_left))
	{
		//打印MP3信息
		printf(" \r\n Bitrate       %dKbps", Mp3FrameInfo.bitrate/1000);
		printf(" \r\n Samprate      %dHz",   Mp3FrameInfo.samprate);
		printf(" \r\n BitsPerSample %db",    Mp3FrameInfo.bitsPerSample);
		printf(" \r\n nChans        %d",     Mp3FrameInfo.nChans);
		printf(" \r\n Layer         %d",     Mp3FrameInfo.layer);
		printf(" \r\n Version       %d",     Mp3FrameInfo.version);
		printf(" \r\n OutputSamps   %d",     Mp3FrameInfo.outputSamps);
		printf("\r\n");
		
		//启动DAC,开始发声
		if (Mp3FrameInfo.nChans == 1)	//单声道要将outputSamps*2
		{
			DAC_DMA_Start(Mp3FrameInfo.samprate, 2 * Mp3FrameInfo.outputSamps);
		}
		else//双声道直接用Mp3FrameInfo.outputSamps
		{
			DAC_DMA_Start(Mp3FrameInfo.samprate, Mp3FrameInfo.outputSamps);
		}
	}
	else //解码失败
	{
		MP3FreeDecoder(Mp3Decoder);
		return;
	}
	
	/* 放音状态 */
	mp3player.ucStatus = STA_PLAYING;
	
	/* 进入主程序循环体 */
	while(mp3player.ucStatus == STA_PLAYING)
	{
			//寻找帧同步,返回第一个同步字的位置
			read_offset = MP3FindSyncWord(read_ptr, bytes_left);
			if(read_offset < 0)					//没有找到同步字
			{
				if(!read_file(mp3file, &read_ptr, &bytes_left))//重新读取一次文件再找
				{
					continue;//回到while(mp3player.ucStatus == STA_PLAYING)后面
				}
			}
			else//找到同步字
			{			
				read_ptr   += read_offset;	//偏移至同步字的位置
				bytes_left -= read_offset;	//同步字之后的数据大小	
				
				if(bytes_left < 1024)				//如果剩余的数据小于1024字节,补充数据
				{
					/* 注意这个地方因为采用的是DMA读取,所以一定要4字节对齐  */
					u16 i = (uint32_t)(bytes_left)&3;	//判断多余的字节
					if(i) i=4-i;						//需要补充的字节
					memcpy(inputbuf+i, read_ptr, bytes_left);	//从对齐位置开始复制
					read_ptr = inputbuf+i;										//指向数据对齐位置
					result = f_read(&file, inputbuf+bytes_left+i, INPUTBUF_SIZE-bytes_left-i, &bw);//补充数据
					if(result != FR_OK)
					{
						printf("读取%s失败 -> %d\r\n",mp3file,result);
						break;
					}
					bytes_left += bw;		//有效数据流大小
				}
			}
			
			//MP3数据解码并送入DAC缓存
			if(!MP3DataDecoder(&read_ptr, &bytes_left))
			{//如果播放出错,Isread置1,避免卡住死循环
				Isread = 1;
			}
			
			//mp3文件读取完成,退出
			if(file.fptr == file.fsize)
			{
				printf("单曲播放完毕\r\n");
				break;
			}	

			//等待DAC发送一半或全部中断
			while(Isread == 0)
			{
				led_delay++;
				if(led_delay == 0xffffff)
				{
					led_delay=0;
					LED1_TROG;
				}
				//Input_scan();		//等待DMA传输完成,此间可以运行按键扫描及处理事件
			}
			Isread = 0;
	}

	//运行到此处,说明单曲播放完成,收尾工作
	DAC_DMA_Stop();//停止喂DAC数据	
	mp3player.ucStatus = STA_IDLE;
	MP3FreeDecoder(Mp3Decoder);//清理缓存
	f_close(&file);	
}

void DMA1_Stream6_IRQHandler(void)
{
	if(DMA_GetITStatus(DMA1_Stream6, DMA_IT_HTIF6) != RESET) //半传输
	{	
		dac_ht = 1;		
		Isread=1;
		
    DMA_ClearITPendingBit(DMA1_Stream6, DMA_IT_HTIF6);
  }
	
	if(DMA_GetITStatus(DMA1_Stream6, DMA_IT_TCIF6) != RESET) //全传输
	{
		dac_ht = 0;
		Isread=1;
		
    DMA_ClearITPendingBit(DMA1_Stream6, DMA_IT_TCIF6);
  }
}

/***************************** (END OF FILE) *********************************/

源码:main.c

/**
  ******************************************************************************
  * @file    ../User/main.c 
  * @author  ZL
  * @version V1.0
  * @date    2015-12-26
  * @brief   Main program body
  ******************************************************************************
**/

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "hw_includes.h"
#include "ff.h"  
#include "exfuns.h"  
#include "mp3Player.h"

//遍历目录文件并打印输出
u8 scan_files(u8 * path)
{
	FRESULT res;
	char buf[512] = {0};	
  char *fn;
	
#if _USE_LFN
 	fileinfo.lfsize = _MAX_LFN * 2 + 1;
	fileinfo.lfname = buf;
#endif
 
	res = f_opendir(&dir,(const TCHAR*)path);
	if (res == FR_OK) 
	{	
		printf("\r\n"); 
		
		while(1){
			
			res = f_readdir(&dir, &fileinfo);                
			if (res != FR_OK || fileinfo.fname[0] == 0) break;  
 
#if _USE_LFN
			fn = *fileinfo.lfname ? fileinfo.lfname : fileinfo.fname;
#else							   
			fn = fileinfo.fname;
#endif	    

			printf("%s/", path);			
			printf("%s\r\n", fn);			
		} 
  }	  
 
  return res;	  
}

/**
  * @brief  Main program
  * @param  None
  * @retval None
  */
int main(void)
{	
	delay_init(168);
	usart1_Init(115200);
	LED_Init();
	DAC_Config();

	if(!SD_Init())
 	{
		exfuns_init();							//为fatfs相关变量申请内存				 
		f_mount(fs[0],"0:",1); 					//挂载SD卡 
	}

	//打印SD目录和文件
	scan_files("0:");
	
	LED0_ON;
	 		
	while (1)
	{
		mp3PlayerDemo("0:/断桥残雪.MP3");
		mp3PlayerDemo("0:/张国荣-玻璃之情.MP3");

		delay_ms(50);
	}
}

为方便调试测试,使用usart1打印数据。实测效果:

程序源码与原理图,测试音频:

链接:https://pan.baidu.com/s/10hYXkrqnuBQgs0DWKLUUOA?pwd=iatt 
提取码:iatt

知道这里下载要积分登录什么的麻烦得很,所以程序放到百度网盘了,假如连接失效,记得在评论区喊我更新!

理论上STM32F1或者其他系列也能用这个方案,要自己改改测试喽,本文把思路分享出来抛砖引玉。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/882494.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mariadb高可用MHA

本节主要学习了Mariadb高可用MHA的概述&#xff0c;案例如何构建MHA 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、概述 1、概念 MHA&#xff08;MasterHigh Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。…

如何使用Kali Linux进行渗透测试?

1. 渗透测试简介 渗透测试是通过模拟恶意攻击&#xff0c;评估系统、应用或网络的安全性的过程。Kali Linux为渗透测试人员提供了丰富的工具和资源&#xff0c;用于发现漏洞、弱点和安全风险。 2. 使用Kali Linux进行渗透测试的步骤 以下是使用Kali Linux进行渗透测试的基本…

搭建WebDAV服务手机ES文件浏览器远程访问

文章目录 1. 安装启用WebDAV2. 安装cpolar3. 配置公网访问地址4. 公网测试连接5. 固定连接公网地址6. 使用固定地址测试连接 有时候我们想通过移动设备访问群晖NAS 中的文件,以满足特殊需求,我们在群辉中开启WebDav服务,结合cpolar内网工具生成的公网地址,通过移动客户端ES文件…

【Unity每日一记】进行发射,位置相关的方法总结

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

绘制世界地图or中国地图

写在前面 在8月初,自己需要使用中国地图的图形,自己就此也查询相关的教程,自己也做一下小小总结,希望对自己和同学们有所帮助。 最终图形 这个系列从2022年开始,一直更新使用R语言分析数据及绘制精美图形。小杜的生信笔记主要分享小杜学习日常!如果,你对此感兴趣可以加…

【C++面向对象】--- 继承 的奥秘(下篇)

个人主页&#xff1a;平行线也会相交&#x1f4aa; 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【C之路】&#x1f48c; 本专栏旨在记录C的学习路线&#xff0c;望对大家有所帮助&#x1f647;‍ 希望我们一起努力、成长&…

自动化测试用例设计实例

在编写用例之间&#xff0c;笔者再次强调几点编写自动化测试用例的原则&#xff1a; 1、一个脚本是一个完整的场景&#xff0c;从用户登陆操作到用户退出系统关闭浏览器。 2、一个脚本脚本只验证一个功能点&#xff0c;不要试图用户登陆系统后把所有的功能都进行验证再退出系统…

CAS 的执行流程 ?CAS 中 ABA 问题如何解决 ?CAS 在 Java 中有哪些实现类 ?

目录 1. CAS 的执行流程 2. CAS 中的 ABA 问题 3. 如何解决 CAS 中的 ABA 问题 4.CAS 在Java 中的实现类有哪些 1. CAS 的执行流程 CAS 比较并替换的大致流程是这样的&#xff1a; 它有三个操作单位&#xff1a;V&#xff08;内存值&#xff09;&#xff0c;A&#xff08;…

【C++】做一个飞机空战小游戏(八)——生成敌方炮弹(rand()和srand()函数应用)

[导读]本系列博文内容链接如下&#xff1a; 【C】做一个飞机空战小游戏(一)——使用getch()函数获得键盘码值 【C】做一个飞机空战小游戏(二)——利用getch()函数实现键盘控制单个字符移动【C】做一个飞机空战小游戏(三)——getch()函数控制任意造型飞机图标移动 【C】做一个飞…

Iceberg 学习笔记

本博客对应于 B 站尚硅谷教学视频 尚硅谷数据湖Iceberg实战教程&#xff08;尚硅谷&Apache Iceberg官方联合推出&#xff09;&#xff0c;为视频对应笔记的相关整理。 1. Iceberg简介 1.1 概述 为了解决数据存储和计算引擎之间的适配的问题&#xff0c;Netflix 开发了 I…

【算法题】螺旋矩阵IV (求解n阶折线蛇形矩阵)

一、问题的提出 n阶折线蛇形矩阵的特点是按照图1所示的方式排列元素。n阶蛇形矩阵是指矩阵的大小为nn&#xff0c;其中n为正整数。 题目背景 一个 n 行 n 列的螺旋矩阵可由如图1所示的方法生成&#xff0c;观察图片&#xff0c;找出填数规律。填数规则为从 1 开始填到 nn。 …

如何使用AIGC人工智能辅助开发?

文章目录 引言AIGC辅助开发的应用场景代码生成图像识别与生成自然语言处理视频剪辑与生成 AIGC辅助开发的实现步骤数据准备模型选择模型训练结果评估与优化应用开发 AIGC辅助开发的优势与局限优势局限 未来展望总结 &#x1f389;欢迎来到AIGC人工智能专栏~如何使用AIGC人工智能…

计算机网络----CRC冗余码的运算

目录 1. 冗余码的介绍及原理2. CRC检验编码的例子3. 小练习 1. 冗余码的介绍及原理 冗余码是用于在数据链路层的通信链路和传输数据过程中可能会出错的一种检错编码方法&#xff08;检错码&#xff09;。原理&#xff1a;发送发把数据划分为组&#xff0c;设每组K个比特&#…

【Python】解决“Tk_GetPixmap: Error from CreateDIBSection”闪退问题

解决Python使用Tkinter的Notebook切换标签时出现的“Tk_GetPixmap: Error from CreateDIBSection 操作成功完成”闪退问题 零、问题描述 在使用Tkinter的Notebook控件时&#xff0c;对其标签进行切换&#xff0c;发现切换不了&#xff0c;一切换就报如下图错误&#xff1a; …

Python学习笔记_基础篇(五)_数据类型之字典

一.基本数据类型 整数&#xff1a;int 字符串&#xff1a;str(注&#xff1a;\t等于一个tab键) 布尔值&#xff1a; bool 列表&#xff1a;list 列表用[] 元祖&#xff1a;tuple 元祖用&#xff08;&#xff09; 字典&#xff1a;dict 注&#xff1a;所有的数据类型都存在想对…

3. 爬取自己CSDN博客列表(分页查询)(网站反爬虫策略,需要在代码中添加合适的请求头User-Agent,否则response返回空)

文章目录 步骤打开谷歌浏览器输入网址按F12进入调试界面点击网络&#xff0c;清除历史消息按F5刷新页面找到接口&#xff08;community/home-api/v1/get-business-list&#xff09;接口解读 撰写代码获取博客列表先明确返回信息格式json字段解读 Apipost测试接口编写python代码…

每天一道leetcode:646. 最长数对链(动态规划中等)

今日份题目&#xff1a; 给你一个由 n 个数对组成的数对数组 pairs &#xff0c;其中 pairs[i] [lefti, righti] 且 lefti < righti 。 现在&#xff0c;我们定义一种 跟随 关系&#xff0c;当且仅当 b < c 时&#xff0c;数对 p2 [c, d] 才可以跟在 p1 [a, b] 后面…

JMeter 特殊组件-逻辑控制器与BeanShell PreProcessor 使用示例

文章目录 前言JMeter 特殊组件-逻辑控制器与BeanShell PreProcessor 使用示例1. 逻辑控制器使用1.1. While Controller 使用示例1.2. 如果&#xff08;If&#xff09;控制器 使用示例 2. BeanShell PreProcessor 使用示例 前言 如果您觉得有用的话&#xff0c;记得给博主点个赞…

视频云存储平台EasyCVR视频汇聚接入AI算法接口,如何在检测中对视频流画框?

视频集中存储EasyCVR安防监控视频汇聚平台基于云边端智能协同架构&#xff0c;具有强大的数据接入、处理及分发能力&#xff0c;平台可支持多协议接入&#xff0c;包括市场主流标准协议与厂家私有协议及SDK&#xff0c;如&#xff1a;国标GB28181、RTMP、RTSP/Onvif、海康Ehome…

Matlab中图例的位置(图例放在图的上方、下方、左方、右方、图外面)等

一、图例默认位置 默认的位置在NorthEast r 10; a 0; b 0; t0:0.1:2.1*pi; xar*cos(t); ybr*sin(t); A1plot(x,y,r,linewidth,4);%圆 hold on axis equal A2plot([0 0],[1 10],b,linewidth,4);%直线 legend([A1,A2],圆形,line)二、通过Location对legend的位置进行改变 变…