AIR32F103(六) ADC,I2S,DMA和ADPCM實現的錄音播放功能

来源:https://www.cnblogs.com/milton/archive/2022/11/23/16919589.html
-Advertisement-
Play Games

使用的MCU型號為 AIR32F103CCT6. 通過工作機制和示例代碼, 說明如何使用AIR32自帶的記憶體實現簡單的語音錄製和播放功能, 以及使用 ADPCM 對音頻數據進行壓縮, 提高錄製時長. 通過這些機制, 可以快速擴充為實用的錄製設備, 例如外掛I2C或SPI存儲, 或提升無線傳輸的音質,... ...


目錄

關於

使用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存儲, 或提升無線傳輸的音質, 在同樣的碼率下使用更高採樣率.


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 用python爬蟲技術,爬取百度搜索結果數據,包含欄位: 頁碼、標題、百度鏈接、真實鏈接、簡介、網站名稱。 並把源碼封裝成exe文件,方便沒有python環境,或者不懂技術的人使用它。 ...
  • 上篇隨筆發佈後,一天內,十幾個爬蟲網站爬取了我的隨筆,有些網站非但沒有註明來源,反而將自己標為博文的原創者,並更改了圖像水印,這篇被爬的隨筆雖瀏覽量不大,但好歹是自己一字一字認真碼出來的,被偷實在令人心情不爽,在本篇隨筆記錄一下。 ...
  • WebDAV 是超文本傳輸協議 (HTTP) 的一組擴展,為 Internet 上電腦之間的編輯和文件管理提供了標準.利用這個協議用戶可以通過Web進行遠程的基本文件操作,如拷貝、移動、刪除等。 在IIS 7.0中,WebDAV是作為獨立擴展模塊,需要單獨進行下載,而IIS 7.5以及以上版本中... ...
  • .NET 現在支持跨平臺這件事情已經是眾所周知的特點了,雖然平臺整體支持跨平臺了,但是我們的代碼如果真的想要實現跨平臺運行其實還是有些小細節要註意的,今天想要記錄分享的就是關於 文件I/O操作時路徑的拼接問題。 在 Windows 環境下我們常見的路徑格式如下: D:\Software\AppDat ...
  • 本文是 《精讀 Mastering ABP Framework》 2.3 探索橫切關註點 - 使用授權和許可權系統 一節的擴充內容,重點探討了授權在分散式和微服務系統中遇到的挑戰,以及 ABP Framework 中採用的解決方案。 ...
  • 在所有監控 Linux 系統性能的工具中,Linux 的 top 命令是最好的也是最知名的一個(htop 是其升級版)。top 命令提供了 Linux 系統運行中的進程的動態實時視圖。它能顯示系統的概覽信息和 Linux 內核當前管理的進程列表。它顯示了大量的系統信息,如 CPU 使用、記憶體使用、交... ...
  • Docker和Docker-Compose簡單搭建與基本設置 一、搭建 Docker 1.第一步當然是看官方文檔 2.然後發現一大堆手動搭建的教程(然後小白就這樣搭建了),然後發現最後有一鍵搭建腳本 curl -fsSL https://get.docker.com -o get-docker.sh ...
  • 字元設備是按照位元組流進行讀寫操作的設備,讀寫數據是分先後順序的。常見的點燈、按鍵、 IIC、 SPI和LCD 等都是字元設備 。 字元設備驅動開發步驟: 總體思路: 定義並初始化一個字元設備 -1、定義一個字元設備—>struct cdev 2、定義並初始化字元設備的文件操作集—>struct fi ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...