目录

关于

使用AIR32的ADC, I2S 和 DMA 实现简单的语音录音和播放功能, 以及使用 ADPCM 编码提升录音时长. 使用的MCU型号为 AIR32F103CCT6. 如果用CBT6, 对应的音频数据数组大小需要相应减小.

音频录音和播放

工作方式

加电后开始录音, 录音结束后循环播放

  • 录音: 麦克风模块 -> ADC采样(12bit, 8K, 11K 或 16K) -> 存储在内存
  • 播放: I2S -> I2S外设(MAX98357A / PT8211) -> 喇叭

对中间每个环节的说明

存储

首先是存储, MCU的内存有限, 如果不借助AT24C, MX25L这类外部存储, 只用内存存储的数据是有限的, AIR32F103CCT6 带 64K Byte内存, 如果按原始采样值存储, 录音时长为

  • 16bit

    • 8K: 128kbps, 约4秒
    • 11K: 176kbps, 约3秒
    • 16K: 256kbps, 约2秒
  • 8bit
    • 8K: 64kbps, 约8秒
    • 11K: 88kbps, 约6秒
    • 16K: 128kbps, 约4秒

采样

使用AIR32的ADC, 配合定时器实现精确的每秒8K, 11K和16K采样. AIR32的ADC分辨率和STM32F103一样都是固定的12bit(STM32F4之后才可以用寄存器调节分辨率)

  • 如果使用ADC的中断, 可以向高位偏移做成16bit, 也可以去掉低位做成8bit
  • 如果使用DMA, 因为AIR32不能像STM32那样, 在4字节地址上偏移一个字节取值, 所以只能按16bit(halfword)传值

音频采集设备如果直接用驻极体话筒, 采样的信号很弱(不是没有, 但是非常小), 需要加一个三极管做放大. 也可以买成品的 MAX9814 模块. 两者的效果区别不大, 但是在调试阶段, 建议用 MAX9814, 因为不用担心信号是否过饱和和失真问题, 在调通之后, 再换回低成本的驻极体话筒和三极管.

驻极体话筒放大的电路和元件参数可以参考这一篇 https://www.cnblogs.com/milton/p/15315783.html

播放

播放可以使用PWM转DAC, 也可以直接用I2S.

  • 如果使用PWM, 因为PWM本身是方波, 会产生大量的谐振噪音, 只有将PWM频率设置到16KHz以上才能明显降低噪音(因为谐振频率超出人耳的听觉范围了), 用8KHz时的噪音非常明显.
  • 因为AIR32F103全系列都支持I2S(数据手册上写只有RPT7才有, 实际上CBT6和CCT6也有), 所以直接用I2S输出是最简单的. 这时候需要一个能接收I2S输出并转为音频的模块.

I2S模块可以用 MAX98357A 模块, 自带I2S解码和放大可以直连喇叭, 也可以买PT8211/TM8211/GH8211, 0.3元一片非常便宜还是双声道, 缺点是不带功放, 如果直连喇叭得贴着耳朵才能听到, 可以再加一个LM386或者PAM8403做放大, 都非常便宜.

实现

硬件

  • AIR32F103CCT6
  • MAX9814
  • PT8211
  • 8欧小喇叭

接线

 *   AIR32F103                  MAX98357A / PT8211
* PB13(SPI1_SCK/I2S_CK) -> BCLK, BCK
* PB15(SPI1_MOSI/I2S_SD) -> DIN
* PB12(SPI1_NSS/I2S_WS) -> LRC, WS
* GND -> GND
* VIN -> 3.3V
* + -> speaker
* - -> speaker
*
* AIR32F103 MAX9814
* PA2 -> Out
* 3.3V -> VDD
* GND -> GND
* GND -> A/R
* GAIN -> float:60dB, gnd:50dB, 3.3v:40dB

代码

完整的示例代码

定义了全局变量

// 定义不同的AUDIO_FREQ值, 可以切换不同的采样频率, 8K, 11K, 16K, 越高的采样频率, 音质越好, 录音时长越短
#define AUDIO_FREQ 8000
//#define AUDIO_FREQ 11000
//#define AUDIO_FREQ 16000 // 定义存储的音频数据大小, CCT6用的是30000, CBT6 或 RPT6 可以相应的减小或增大
#define BUFF_SIZE 30000 // 音频数据数组, 同时用于DMA的接收地址
uint16_t dma_buf[BUFF_SIZE]; // I2S传输时, 用于记录传输的位置
uint32_t index;
// I2S传输时, 用于区分左右声道
__IO uint8_t lr = 0;

初始化GPIO, PA2是采样输入, PB12, PB13, PB15 用于I2S传输, PC13 是板载的LED, 用于指示录音开始和结束. 如果使用的不是Bluepill而是合宙的开发板, 可以修改为开发板对应的LED GPIO.

void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// PA2 as analog input
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// PB12,PB13,PB15 as I2S AF output
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// PC13 as GPIO output
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC, &GPIO_InitStructure);
}

初始化ADC, 设置为外部触发模式, 这里使用TIM3的Update中断作为触发源, 初始化之后ADC并不会立即开始转换, 而是在TIM3的每次Update中断时进行转换. 所以如果要停止ADC, 需要先停掉TIM3

void ADC_Configuration(void)
{
ADC_InitTypeDef ADC_InitStructure; // Reset ADC1
ADC_DeInit(ADC1);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
// 设置 TIM3 为外置触发源
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO;
// 结果右对齐
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
// 只使用一个通道
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
// PA2对应的channel是 ADC_Channel_2
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 1, ADC_SampleTime_239Cycles5); // 启用ADC1的外部触发源
ADC_ExternalTrigConvCmd(ADC1, ENABLE); // 在 ADC1 上启用 DMA
ADC_DMACmd(ADC1, ENABLE);
ADC_Cmd(ADC1, ENABLE); // 校准
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1));
}

初始化DMA, 用 ADC1->DR 作为外设地址, dma_buf作为内存地址, 内存地址递增, 数据大小为16bit, 循环填充. 同时打开DMA的填充完成中断 DMA_IT_TC

//调用
DMA_Configuration(DMA1_Channel1, (uint32_t)&ADC1->DR, (uint32_t)dma_buf, BUFF_SIZE); // 函数实现
void DMA_Configuration(DMA_Channel_TypeDef *DMA_CHx, uint32_t ppadr, uint32_t memadr, uint16_t bufsize)
{
DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA_CHx);
DMA_InitStructure.DMA_PeripheralBaseAddr = ppadr;
DMA_InitStructure.DMA_MemoryBaseAddr = memadr;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = bufsize;
// Addresss increase - peripheral:no, memory:yes
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
// Data unit size: 16bit
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
// Memory to memory: no
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA_CHx, &DMA_InitStructure);
// Enable 'Transfer complete' interrupt
DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
// Enable DMA
DMA_Cmd(DMA_CHx, ENABLE);
}

打开外设的中断控制, DMA用于转换结束, SPI2的中断用于每次的数据发送

void NVIC_Configuration(void)
{
// DMA1 interrupts
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 6;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// SPI2 interrupts
NVIC_InitStructure.NVIC_IRQChannel = SPI2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 6;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}

初始化定时器TIM3, 根据MCU频率72MHz, 计算得到分别在8K, 11K, 16K时的定时器周期和预分频系数. 启用计时器的Update中断, 但是不启动定时器, 因为启动后就会产生中断, 就会触发ADC转换. 需要将计时器的启动放到main()中.

void TIM_Configuration(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = 9 - 1;
#if AUDIO_FREQ == 8000
// Period = 72,000,000 / 8,000 = 1000 * 9
TIM_TimeBaseStructure.TIM_Prescaler = 1000 - 1;
#elif AUDIO_FREQ == 11000
// Period = 72,000,000 / 11,000 = 727 * 9
TIM_TimeBaseStructure.TIM_Prescaler = 727 - 1;
#else
// Period = 72,000,000 / 16,000 = 500 * 9
TIM_TimeBaseStructure.TIM_Prescaler = 500 - 1;
#endif
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// Enable TIM3 'TIM update' trigger for adc
TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update);
// Timer will be started in main()
}

初始化I2S, 如果使用的是PT8211, 需要将 I2S_Standard 设置为 I2S_Standard_LSB. 否则双声道传数据时工作不正常

void IIS_Configuration(void)
{
I2S_InitTypeDef I2S_InitStructure; SPI_I2S_DeInit(SPI2);
I2S_InitStructure.I2S_Mode = I2S_Mode_MasterTx;
// PT8211:LSB, MAX98357A:Phillips
I2S_InitStructure.I2S_Standard = I2S_Standard_Phillips;
// 16-bit data resolution
I2S_InitStructure.I2S_DataFormat = I2S_DataFormat_16b;
#if AUDIO_FREQ == 8000
// 8K sampling rate
I2S_InitStructure.I2S_AudioFreq = I2S_AudioFreq_8k;
#elif AUDIO_FREQ == 11000
// 11K sampling rate
I2S_InitStructure.I2S_AudioFreq = I2S_AudioFreq_11k;
#else
// 16K sampling rate
I2S_InitStructure.I2S_AudioFreq = I2S_AudioFreq_16k;
#endif
I2S_InitStructure.I2S_CPOL = I2S_CPOL_Low;
I2S_InitStructure.I2S_MCLKOutput = I2S_MCLKOutput_Disable;
I2S_Init(SPI2, &I2S_InitStructure); I2S_Cmd(SPI2, ENABLE);
}

中断处理

  • DMA中断: DMA中断时表示内存数组已经装满了, 此时要停掉TIM3和ADC1, 并关掉PC13 LED指示录音结束
void DMA1_Channel1_IRQHandler(void)
{
// DMA1 Channel1 Transfer Complete interrupt
if (DMA_GetITStatus(DMA1_IT_TC1))
{
DMA_ClearITPendingBit(DMA1_IT_GL1);
// Stop ADC(by stopping TIM3)
TIM_Cmd(TIM3, DISABLE);
ADC_Cmd(ADC1, DISABLE);
GPIO_SetBits(GPIOC, GPIO_Pin_13);
}
}
  • SPI2(I2S)中断, 用于每个I2S数据的传输, 因为传输时左右声道的数据是交替传输的, 所以这里需要用一个全局变量切换当前的声道. 因为录音是单声道, 所以传输时对应两个声道, 每个值会被传输两遍. 到达最后一个值后, 会停掉I2S.
void SPI2_IRQHandler(void)
{
// If TX Empty flag is set
if (SPI_I2S_GetITStatus(SPI2, SPI_I2S_IT_TXE) == SET)
{
// Put data to both channels
if (lr == 0)
{
lr = 1;
SPI_I2S_SendData(SPI2, (uint16_t)dma_buf[index] << 3);
}
else
{
lr = 0;
SPI_I2S_SendData(SPI2, (uint16_t)dma_buf[index++] << 3);
if (index == BUFF_SIZE)
{
index = 0;
// Disable the I2S1 TXE Interrupt to stop playing
SPI_I2S_ITConfig(SPI2, SPI_I2S_IT_TXE, DISABLE);
}
}
}
}

主函数. 在主函数中, 先开启录音, 然后等待4秒(对应 3万个样本, 8K采样, 4秒之内就结束了), 然后开始播放. 每个循环等待5秒. 播放会在中断中判断是否结束, 结束就停止.

int main(void)
{
Delay_Init();
USART_Printf_Init(115200);
printf("SystemClk:%ld\r\n", SystemCoreClock); RCC_Configuration();
GPIO_Configuration();
ADC_Configuration();
DMA_Configuration(DMA1_Channel1, (uint32_t)&ADC1->DR, (uint32_t)dma_buf, BUFF_SIZE);
NVIC_Configuration();
TIM_Configuration();
IIS_Configuration();
GPIO_SetBits(GPIOC, GPIO_Pin_13);
Delay_S(1);
// Start timer to start recording
printf("Start recording\r\n");
TIM_Cmd(TIM3, ENABLE);
// Turn on LED, DMA TC1 interrupt will turn it off
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
Delay_S(4);
printf("Start playing\r\n");
while (1)
{
// Restart the playing
SPI_I2S_ITConfig(SPI2, SPI_I2S_IT_TXE, ENABLE);
Delay_S(5);
}
}

使用ADPCM压缩音频数据

ADPCM 的原理和计算方式可以参考这一篇 https://www.cnblogs.com/milton/p/16914797.html.

使用ADPCM可以将16bit的数据压缩为4bit, 同时保持基本一致的听觉信息. 这样对于64kB的CCT6, 可以在12bit的效果下记录接近16秒的语音(64K = 16 * 8K * 0.5). 而且 ADPCM 的计算简单, AIR32这种M3核心的MCU处理起来非常轻松.

工作机制调整

如果使用ADPCM, 需要对前面的例子进行一些调整. 硬件和前面的一致, 改动都在代码.

去掉DMA

因为DMA必须是硬件到硬件, 如果想做成双缓冲, 比如做一个1K左右的DMA数组, 一半结束后批量编码, 再等另一半结束再编码? 这样其实不行, 因为集中编码时ADC也还在进行, 一边在计算一边在转换和中断, 会互相影响, 导致采样不均匀. 因为ADC转换使用定时器触发, 定时器两个中断之间, ADC转换的时间很短, 中间间隔的时间完全可以用于编码, 所以需要将DMA去掉, 改成使用ADC的转换完成中断, 在完成中断的处理函数中对采样值进行编码

调整数组

为了计算方便, 将语音数组转换为uint8_t, 这样每个值记录的是两个采样点, 相应的数组大小扩充到了60000

调整I2S传输

因为每个值存储的是两个采样, 因此在I2S的TXE中断处理中, 原先的左右声道判断需要叠加4bit偏移判断, 变成4种情况.

代码

完整的示例代码

ADC启用中断

void ADC_Configuration(void)
{
ADC_InitTypeDef ADC_InitStructure; // Reset ADC1
ADC_DeInit(ADC1);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
// Select TIM3 trigger output as external trigger
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
// ADC_Channel_2 for PA2
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 1, ADC_SampleTime_7Cycles5); // Enable ADC1 external trigger
ADC_ExternalTrigConvCmd(ADC1, ENABLE);
ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE); // Enable ADC1
ADC_Cmd(ADC1, ENABLE); // Calibration
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1));
}

在ADC中断中, 对ADC结果值的编码

void Audio_Encode(void)
{
static uint32_t idx = 0;
static uint8_t msb = 0;
uint8_t val; val = ADPCM_Encode((uint16_t)(ADC1->DR << 2)) & 0x0F;
if (msb == 0)
{
voice[idx] = val;
msb = 1;
}
else
{
voice[idx] |= (val << 4);
msb = 0;
idx++;
if (idx == BUFF_SIZE)
{
// Stop ADC(by stopping TIM3)
TIM_Cmd(TIM3, DISABLE);
ADC_Cmd(ADC1, DISABLE);
ADC_ExternalTrigConvCmd(ADC1, DISABLE);
GPIO_SetBits(GPIOC, GPIO_Pin_13);
idx = 0;
finish = 1;
}
}
}

在I2S传输中断中, 对值的解码. 每传输四个数据(低4位左右声道, 高4位左右声道)下标才加1, 传输结束后重置下标.

uint16_t Audio_Decode(void)
{
static uint32_t idx = 0;
static __IO uint8_t msb = 0, lr = 0;
static uint16_t val; if (msb == 0)
{
// Put data to both channels
if (lr == 0)
{
val = ADPCM_Decode(voice[idx] & 0x0F);
lr = 1;
}
else if (lr == 1)
{
lr = 0;
msb = 1;
}
}
else
{
if (lr == 0)
{
val = ADPCM_Decode((voice[idx] >> 4) & 0x0F);
lr = 1;
}
else if (lr == 1)
{
lr = 0;
msb = 0;
idx++;
if (idx == BUFF_SIZE)
{
idx = 0;
ADPCM_Reset();
}
}
}
return val;
}

使用ADPCM后, 在8K采样下语音音质没有明显下降, 但是录音时长增长到了15秒, 提升明显.

最后

以上说明了如何使用AIR32自带的内存实现简单的语音录制和播放功能, 以及使用 ADPCM 对音频数据进行压缩, 提高录制时长. 通过这些机制, 可以快速扩充为实用的录制设备, 例如外挂I2C或SPI存储, 或提升无线传输的音质, 在同样的码率下使用更高采样率.

AIR32F103(六) ADC,I2S,DMA和ADPCM实现录音播放功能的更多相关文章

  1. STM32F103ZET6 之 ADC+TIM+DMA+USART 综合实验

    1.实验目的 1)使用 TIM1 触发 ADC,ADC 采集的数据通过DMA 传至内存,然后通过串口打印出采集的数据: 2)学会 DMA 传输数据并将数据进行保存: 3)验证ADC 的采样率与实际设置 ...

  2. STM32L15x——ADC采集DMA数据只第一次正确(已解决)

    前提:我用的芯片是STM32L系列,可能对其它STM32系列不完全适用,仅供参考! 一.问题描述 我在使用DMA方式读取单ADC单通道采集的数据时,发现只能正确的采集一次数据,后来的就一直与第一次的相 ...

  3. 重学STM32---(六)DAC+DMA+TIM

    这两天复习了DAC,DMA再加上把基本定时器TIM6和TIM7看了一下,打算写一个综合点的程序,,,就在网上找了一些关于DAC,DMA和定时器相关的程序,最终打算写了输出正弦波的程序... 由于没有示 ...

  4. 记STM32F030多通道ADC DMA读取乱序问题

    问题描述通过 uint16_t ConvData[8]保存DMA搬运的ADC转换数值,但是这个数组数值的顺序总是和ADC不是顺序对应的.比如用7个通道的ADC,当设置ADC_InitStructure ...

  5. STM32 DMA USART ADC

    转载自:http://www.cnblogs.com/UQYT/articles/2949794.html 这是一个综合的例子,演示了ADC模块.DMA模块和USART模块的基本使用. 我们在这里设置 ...

  6. STM32学习笔记(七) ADC模数转换测电平(普通和DMA模式)

    嵌入式系统在微控制领域(温度,湿度,压力检测,四轴飞行器)中占据着重要地位,这些功能的实现是由微处理器cpu(如stm32)和传感器以及控制器共同完成的,而连接他们,使它们能够互相正常交流的正是本小节 ...

  7. 电子设计省赛--DMA与ADC

    //2014年4月17日 //2014年6月20日入"未完毕" //2014年6月21日 DMA可实现无需cpu控制中断的传输数据保存. 特别是ADC转换多个通道时要用到. 关键是 ...

  8. 【STM32】用DMA实现多路ADC通道数据采集

    今天尝试了下STM32的ADC采样,并利用DMA实现采样数据的直接搬运存储,这样就不用CPU去参与操作了. 找了不少例子参考,ADC和DMA的设置了解了个大概,并直接利用开发板来做一些实验来验证相关的 ...

  9. 关于Stm32定时器+ADC+DMA进行AD采样的实现

    Stm32的ADC有DMA功能这都毋庸置疑,也是我们用的最多的!然而,如果我们要对一个信号(比如脉搏信号)进行定时采样(也就是隔一段时间,比如说2ms),有三种方法: 1.使用定时器中断每隔一定时间进 ...

  10. stm32之TIM+ADC+DMA采集50HZ交流信号

    http://cache.baiducontent.com/c?m=9d78d513d98207f04fece47f0d01d7174a02d1743ca6c76409c3e03984145b5637 ...

随机推荐

  1. winfrom程序只启动一个exe进程

    private static void KillProcess() { Process process1 = Process.GetCurrentProcess(); //获得当前计算机系统内某个进程 ...

  2. SpringBoot源码学习1——SpringBoot自动装配源码解析+Spring如何处理配置类的

    系列文章目录和关于我 一丶什么是SpringBoot自动装配 SpringBoot通过SPI的机制,在我们程序员引入一些starter之后,扫描外部引用 jar 包中的META-INF/spring. ...

  3. SpringBoot Xml转Json对象

    一.导入需要的依赖 <dependency> <groupId>maven</groupId> <artifactId>dom4j</artifa ...

  4. 配置git环境与项目创建

    主要用于记录上课笔记,方便以后复习 acgit的地址:https://git.acwing.com/wyw/kob1/ 1. 项目模块的包含 1.1 采用前后端分离 Web端大概框架 2. git环境 ...

  5. HTTPS安全加固配置最佳实践指南

    转载自:https://www.bilibili.com/read/cv16067729?spm_id_from=333.999.0.0 0x02 HTTPS安全加固指南 描述: 当你的网站上了 HT ...

  6. Kubernetes 监控--Grafana

    前面我们使用 Prometheus 采集了 Kubernetes 集群中的一些监控数据指标,我们也尝试使用 promQL 语句查询出了一些数据,并且在 Prometheus 的 Dashboard 中 ...

  7. CentOS7下yum安装GitLab-CE

    前提准备 建立git用户 useradd git 关闭防火墙 systemctl stop firewalld systemctl disable firewalld 安装依赖库 yum instal ...

  8. PPR管及管件的类型、规格与选用

    1. PPR管的类型及参数识读 2. 常用的PPR管件及规格 3. 住宅给水管的PPR管及管件的需求量

  9. EFCore分表实现

    实现原理 当我们new一个上下文DbContext 后, 每次执行CURD方式时 ,都会依次调用OnConfiguring(),OnModelCreating()两个方法. OnConfiguring ...

  10. [笔记] 二维FFT

    假设现在有2个矩阵a和b,分别是n行m列和x行y列,现在你要计算它们的二维卷积,也就是求出矩阵s满足: \(s_{i,j}=\sum_{i'\leq i,j'\leq j}a_{i',j'}b_{i- ...