当前位置:首页 > 技术 > 正文内容

STM32入门学习12-软硬件SPI驱动W25Q64闪存

访客 技术 2026年7月16日 1

(一)SPI通信协议概述

SPI与I2C同属常见通信协议,SPI相较于I2C具有更高的传输速率。两者均为同步通信,需要时钟线配合,但SPI采用双数据线架构:一根用于主机发送(MOSI),一根用于主机接收(MISO)。此外,每个SPI从设备都通过独立的片选线(SS)与主机连接,主机通过拉低对应片选信号来选中目标从设备进行通信。

通信协议 时钟线 数据线 传输模式
I2C SCL SDA 半双工
SPI SCK MISO、MOSI 全双工

SPI支持四种工作模式,通过配置时钟极性(CPOL)和时钟相位(CPHA)来决定采样时机和空闲电平。最常用的是模式0:时钟空闲时为低电平,在上升沿进行数据采样,这与I2C的时钟逻辑较为相似。

  • 模式0:CPOL=0,CPHA=0,SCK空闲为低电平,数据在SCK的上升沿被采样。
  • 模式1:CPOL=0,CPHA=1,SCK空闲为低电平,数据在SCK的下降沿被采样。
  • 模式2:CPOL=1,CPHA=0,SCK空闲为高电平,数据在SCK的下降沿被采样。
  • 模式3:CPOL=1,CPHA=1,SCK空闲为高电平,数据在SCK的上升沿被采样。

SPI的特点是发送与接收同时进行。在向指定地址写入数据时,该地址的原有数据会被同时读出,即使应用层并不需要这个数据。SPI同样采用高位先行的传输方式:主机数据的高位通过MOSI传输到从设备的低位,同时从设备的高位通过MISO传输到主机的低位。

(二)软件模拟SPI实现

通过GPIO引脚模拟SPI时序的方式称为软件模拟SPI。通信前需要先选中目标从设备,将片选线(SS)拉低。选用模式0时,SCK默认保持低电平,在上升沿采样数据。本示例中,片选线SS连接PA4,时钟线SCK连接PA5,主机输入MISO连接PA6,主机输出MOSI连接PA7。

#define SPI_SS   GPIO_Pin_4
#define SPI_SCK  GPIO_Pin_5
#define SPI_MISO GPIO_Pin_6
#define SPI_MOSI GPIO_Pin_7

为了简化软件模拟时的操作,封装三个基础函数:设置引脚输出高电平、设置引脚输出低电平、读取MISO引脚输入状态。

void spi_pin_set(uint16_t pin)
{
    GPIO_SetBits(GPIOA, pin);
}

void spi_pin_reset(uint16_t pin)
{
    GPIO_ResetBits(GPIOA, pin);
}

uint8_t spi_pin_read(void)
{
    return GPIO_ReadInputDataBit(GPIOA, SPI_MISO);
}

(1)SPI初始化

对各连接引脚进行初始化配置。SCK、MOSI和SS均配置为推挽输出模式,以获得更快的电平切换速度;MISO配置为上拉输入模式,用于接收从设备返回的数据。

void spi_soft_init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    GPIO_InitTypeDef gpio_init;
    gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
    gpio_init.GPIO_Pin = SPI_SS | SPI_SCK | SPI_MOSI;
    gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_init);
    
    gpio_init.GPIO_Mode = GPIO_Mode_IPU;
    gpio_init.GPIO_Pin = SPI_MISO;
    gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_init);
    
    spi_pin_set(SPI_SS);
    spi_pin_reset(SPI_SCK);
}

初始化完成后,SS置高电平表示未选中任何从设备,SCK置低电平进入空闲状态。

(2)通信开始与结束

通过控制SS引脚电平来选中或释放从设备。开始通信时将SS拉低,结束通信时将SS拉高。

void spi_soft_start(void)
{
    spi_pin_reset(SPI_SS);
}

void spi_soft_end(void)
{
    spi_pin_set(SPI_SS);
}

(3)数据交换

采用模式0时,在SCK上升沿采样数据。从设备会自动将其高位数据放到MISO线上,主机循环执行8次即可完成一个字节的数据交换。

实现思路:在SCK低电平期间将待发送数据放在MOSI线上,然后拉高SCK产生上升沿,此时从设备数据已稳定在MISO线上,读取该引脚电平即可获得数据。最后将SCK恢复为低电平,为下一次交换做准备。

uint8_t spi_soft_exchange(uint8_t tx_data)
{
    uint8_t rx_data = 0x00;
    uint8_t i;
    
    for (i = 0; i < 8; i++)
    {
        if (tx_data & (0x80 >> i))
        {
            spi_pin_set(SPI_MOSI);
        }
        else
        {
            spi_pin_reset(SPI_MOSI);
        }
        
        spi_pin_set(SPI_SCK);
        
        if (spi_pin_read())
        {
            rx_data |= (0x80 >> i);
        }
        
        spi_pin_reset(SPI_SCK);
    }
    
    return rx_data;
}

若仅需发送数据而无需接收返回值,可直接调用此函数并忽略返回值;若需要接收数据,需先发送任意数据(如0xFF)来触发从设备返回数据。

(4)完整代码

将上述函数封装为独立的模块,包含头文件声明和实现文件。

#ifndef __SOFT_SPI_H__
#define __SOFT_SPI_H__

void spi_soft_init(void);
void spi_soft_start(void);
void spi_soft_end(void);
uint8_t spi_soft_exchange(uint8_t tx_data);

#endif
#include "stm32f10x.h"

#define SPI_SS   GPIO_Pin_4
#define SPI_SCK  GPIO_Pin_5
#define SPI_MISO GPIO_Pin_6
#define SPI_MOSI GPIO_Pin_7

void spi_pin_set(uint16_t pin)
{
    GPIO_SetBits(GPIOA, pin);
}

void spi_pin_reset(uint16_t pin)
{
    GPIO_ResetBits(GPIOA, pin);
}

uint8_t spi_pin_read(void)
{
    return GPIO_ReadInputDataBit(GPIOA, SPI_MISO);
}

void spi_soft_init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    GPIO_InitTypeDef gpio_init;
    gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
    gpio_init.GPIO_Pin = SPI_SS | SPI_SCK | SPI_MOSI;
    gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_init);
    
    gpio_init.GPIO_Mode = GPIO_Mode_IPU;
    gpio_init.GPIO_Pin = SPI_MISO;
    gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_init);
    
    spi_pin_set(SPI_SS);
    spi_pin_reset(SPI_SCK);
}

void spi_soft_start(void)
{
    spi_pin_reset(SPI_SS);
}

void spi_soft_end(void)
{
    spi_pin_set(SPI_SS);
}

uint8_t spi_soft_exchange(uint8_t tx_data)
{
    uint8_t rx_data = 0x00;
    uint8_t i;
    
    for (i = 0; i < 8; i++)
    {
        if (tx_data & (0x80 >> i))
        {
            spi_pin_set(SPI_MOSI);
        }
        else
        {
            spi_pin_reset(SPI_MOSI);
        }
        
        spi_pin_set(SPI_SCK);
        
        if (spi_pin_read())
        {
            rx_data |= (0x80 >> i);
        }
        
        spi_pin_reset(SPI_SCK);
    }
    
    return rx_data;
}

(三)硬件SPI驱动

STM32内置SPI硬件控制器,可通过配置寄存器实现SPI通信。使用硬件SPI时,需将对应引脚配置为复用功能。本示例使用SPI1,引脚映射与软件模拟方案相同。

查看标准库提供的SPI相关函数:

void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

(1)SPI初始化

时钟配置

首先使能GPIO和SPI1的外设时钟。SPI1挂载在APB2总线上。

void spi_hw_clock_enable(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
}

GPIO配置

片选信号SS由软件手动控制,配置为推挽输出。SCK和MOSI配置为复用推挽输出,由硬件SPI控制器驱动。MISO配置为上拉输入。

void spi_hw_gpio_init(void)
{
    GPIO_InitTypeDef gpio_init;
    
    gpio_init.GPIO_Pin = GPIO_Pin_4;
    gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
    gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_init);
    
    gpio_init.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
    gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;
    gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_init);
    
    gpio_init.GPIO_Pin = GPIO_Pin_6;
    gpio_init.GPIO_Mode = GPIO_Mode_IPU;
    gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_init);
    
    GPIO_SetBits(GPIOA, GPIO_Pin_4);
}

SPI参数配置

根据模式0的要求配置SPI初始化结构体:双向全双工模式、主机模式、8位数据帧、时钟极性低、第一个边沿采样、软件控制片选、分频系数128、高位先行。

void spi_hw_config(void)
{
    SPI_InitTypeDef spi_init;
    spi_init.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    spi_init.SPI_Mode = SPI_Mode_Master;
    spi_init.SPI_DataSize = SPI_DataSize_8b;
    spi_init.SPI_CPOL = SPI_CPOL_Low;
    spi_init.SPI_CPHA = SPI_CPHA_1Edge;
    spi_init.SPI_NSS = SPI_NSS_Soft;
    spi_init.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
    spi_init.SPI_FirstBit = SPI_FirstBit_MSB;
    spi_init.SPI_CRCPolynomial = 7;
    SPI_Init(SPI1, &spi_init);
}

统一初始化

将各初始化步骤组合,并在最后使能SPI控制器。

void spi_hw_init(void)
{
    spi_hw_clock_enable();
    spi_hw_gpio_init();
    spi_hw_config();
    SPI_Cmd(SPI1, ENABLE);
}

(2)通信控制

通过SS引脚实现从设备的选通和释放。

void spi_hw_start(void)
{
    GPIO_ResetBits(GPIOA, GPIO_Pin_4);
}

void spi_hw_end(void)
{
    GPIO_SetBits(GPIOA, GPIO_Pin_4);
}

(3)数据交换

硬件SPI的数据交换通过状态标志位控制。发送数据前需等待发送缓冲区为空,发送完成后接收缓冲区会自动接收从设备返回的数据,再等待接收缓冲区非空即可读取。

uint8_t spi_hw_transfer(uint8_t tx_byte)
{
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);
    SPI_I2S_SendData(SPI1, tx_byte);
    
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);
    return SPI_I2S_ReceiveData(SPI1);
}

(4)完整代码

#ifndef __HW_SPI_H__
#define __HW_SPI_H__

void spi_hw_init(void);
void spi_hw_start(void);
void spi_hw_end(void);
uint8_t spi_hw_transfer(uint8_t tx_byte);

#endif
#include "stm32f10x.h"

void spi_hw_clock_enable(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
}

void spi_hw_gpio_init(void)
{
    GPIO_InitTypeDef gpio_init;
    
    gpio_init.GPIO_Pin = GPIO_Pin_4;
    gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
    gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_init);
    
    gpio_init.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
    gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;
    gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_init);
    
    gpio_init.GPIO_Pin = GPIO_Pin_6;
    gpio_init.GPIO_Mode = GPIO_Mode_IPU;
    gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_init);
    
    GPIO_SetBits(GPIOA, GPIO_Pin_4);
}

void spi_hw_config(void)
{
    SPI_InitTypeDef spi_init;
    spi_init.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    spi_init.SPI_Mode = SPI_Mode_Master;
    spi_init.SPI_DataSize = SPI_DataSize_8b;
    spi_init.SPI_CPOL = SPI_CPOL_Low;
    spi_init.SPI_CPHA = SPI_CPHA_1Edge;
    spi_init.SPI_NSS = SPI_NSS_Soft;
    spi_init.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
    spi_init.SPI_FirstBit = SPI_FirstBit_MSB;
    spi_init.SPI_CRCPolynomial = 7;
    SPI_Init(SPI1, &spi_init);
}

void spi_hw_init(void)
{
    spi_hw_clock_enable();
    spi_hw_gpio_init();
    spi_hw_config();
    SPI_Cmd(SPI1, ENABLE);
}

void spi_hw_start(void)
{
    GPIO_ResetBits(GPIOA, GPIO_Pin_4);
}

void spi_hw_end(void)
{
    GPIO_SetBits(GPIOA, GPIO_Pin_4);
}

uint8_t spi_hw_transfer(uint8_t tx_byte)
{
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);
    SPI_I2S_SendData(SPI1, tx_byte);
    
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);
    return SPI_I2S_ReceiveData(SPI1);
}

(四)W25Q64闪存操作

W25Q64是一款容量为64Mbit的串行Flash存储器,具有掉电数据不丢失特性,支持SPI模式0和模式3。其存储空间较大,地址宽度为24位。操作W25Q64需遵循以下规则:

(1)每次操作前需先发送指令码,再发送地址码指定目标区域;

(2)写入数据前必须先发送写使能指令;

(3)写入前必须先擦除对应区域,擦除后所有位变为1;

(4)擦除和写入操作需要一定时间,此时芯片处于忙状态,无法响应新的读写请求;

主要指令码包括:读取状态寄存器(0x05)、写使能(0x06)、页编程(0x02)、读取数据(0x03)、扇区擦除(0x20)等。

(1)等待芯片空闲

进行任何操作前都需检查芯片状态,确保不在忙碌状态。读取状态寄存器,判断最低位是否为0。

void flash_wait_ready(void)
{
    spi_hw_start();
    spi_hw_transfer(0x05);
    while ((spi_hw_transfer(0xFF) & 0x01) != 0);
    spi_hw_end();
}

(2)写入数据

W25Q64支持连续地址自动递增,可一次写入多个字节。写入前需等待芯片就绪,发送写使能指令,然后发送页编程指令和24位起始地址,最后写入数据。

W25Q64存储结构划分为块、扇区和页。整片8MB存储空间包含128个64KB的块,每块包含16个4KB的扇区,每扇区包含16个256字节的页。写入超过一页长度的数据会导致地址回绕,覆盖页起始位置的数据。

void flash_write_page(uint32_t addr, uint8_t* data_buf, uint8_t len)
{
    uint8_t i;
    
    flash_wait_ready();
    
    spi_hw_start();
    spi_hw_transfer(0x06);
    spi_hw_end();
    
    spi_hw_start();
    spi_hw_transfer(0x02);
    spi_hw_transfer(addr >> 16);
    spi_hw_transfer(addr >> 8);
    spi_hw_transfer(addr);
    
    for (i = 0; i < len; i++)
    {
        spi_hw_transfer(data_buf[i]);
    }
    spi_hw_end();
}

地址右移16位得到高8位,右移8位得到中8位,保留低8位。SPI传输采用高位先行方式。

(3)读取数据

读取操作同样需先检查芯片状态,然后发送读取指令和起始地址,随后连续读取数据,地址会自动递增。

void flash_read_data(uint32_t addr, uint8_t* data_buf, uint32_t len)
{
    uint32_t i;
    
    flash_wait_ready();
    
    spi_hw_start();
    spi_hw_transfer(0x03);
    spi_hw_transfer(addr >> 16);
    spi_hw_transfer(addr >> 8);
    spi_hw_transfer(addr);
    
    for (i = 0; i < len; i++)
    {
        data_buf[i] = spi_hw_transfer(0xFF);
    }
    spi_hw_end();
}

(4)扇区擦除

W25Q64不支持字节或页级别的单独擦除,只能按扇区(4KB)、块(64KB)或整片进行擦除。以下函数实现扇区擦除,擦除后该扇区内所有数据位变为1。

void flash_erase_sector(uint32_t addr)
{
    flash_wait_ready();
    
    spi_hw_start();
    spi_hw_transfer(0x06);
    spi_hw_end();
    
    spi_hw_start();
    spi_hw_transfer(0x20);
    spi_hw_transfer(addr >> 16);
    spi_hw_transfer(addr >> 8);
    spi_hw_transfer(addr);
    spi_hw_end();
}

(5)完整驱动代码

将SPI初始化与Flash操作函数封装在一起,便于统一调用。

#ifndef __W25Q64_H__
#define __W25Q64_H__

void flash_init(void);
void flash_write_page(uint32_t addr, uint8_t* data_buf, uint8_t len);
void flash_read_data(uint32_t addr, uint8_t* data_buf, uint32_t len);
void flash_erase_sector(uint32_t addr);

#endif
#include "stm32f10x.h"
#include "hw_spi.h"

void flash_init(void)
{
    spi_hw_init();
}

void flash_wait_ready(void)
{
    spi_hw_start();
    spi_hw_transfer(0x05);
    while ((spi_hw_transfer(0xFF) & 0x01) != 0);
    spi_hw_end();
}

void flash_write_page(uint32_t addr, uint8_t* data_buf, uint8_t len)
{
    uint8_t i;
    
    flash_wait_ready();
    
    spi_hw_start();
    spi_hw_transfer(0x06);
    spi_hw_end();
    
    spi_hw_start();
    spi_hw_transfer(0x02);
    spi_hw_transfer(addr >> 16);
    spi_hw_transfer(addr >> 8);
    spi_hw_transfer(addr);
    
    for (i = 0; i < len; i++)
    {
        spi_hw_transfer(data_buf[i]);
    }
    spi_hw_end();
}

void flash_read_data(uint32_t addr, uint8_t* data_buf, uint32_t len)
{
    uint32_t i;
    
    flash_wait_ready();
    
    spi_hw_start();
    spi_hw_transfer(0x03);
    spi_hw_transfer(addr >> 16);
    spi_hw_transfer(addr >> 8);
    spi_hw_transfer(addr);
    
    for (i = 0; i < len; i++)
    {
        data_buf[i] = spi_hw_transfer(0xFF);
    }
    spi_hw_end();
}

void flash_erase_sector(uint32_t addr)
{
    flash_wait_ready();
    
    spi_hw_start();
    spi_hw_transfer(0x06);
    spi_hw_end();
    
    spi_hw_start();
    spi_hw_transfer(0x20);
    spi_hw_transfer(addr >> 16);
    spi_hw_transfer(addr >> 8);
    spi_hw_transfer(addr);
    spi_hw_end();
}

(五)功能验证

在主函数中测试Flash的读写功能。先擦除指定扇区,然后写入测试数据,再读取验证。

#include "stm32f10x.h"
#include "OLED.h"
#include "w25q64.h"

int main(void)
{
    uint8_t write_buf[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    uint8_t read_buf[5];
    uint8_t i;
    
    OLED_Init();
    flash_init();
    
    flash_erase_sector(0x00000000);
    flash_write_page(0x00000000, write_buf, 10);
    flash_read_data(0x00000000, read_buf, 5);
    
    OLED_ShowString(1, 1, "Write:");
    for (i = 0; i < 10; i++)
    {
        OLED_ShowNum(2, i + 1, write_buf[i], 1);
    }
    
    OLED_ShowString(3, 1, "Read:");
    for (i = 0; i < 5; i++)
    {
        OLED_ShowNum(4, i + 1, read_buf[i], 1);
    }
    
    while (1);
    return 0;
}
标签: STM32

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。