当前位置: 首页 > news >正文

STM32F103C8T6硬件SPI驱动W25Q64 Flash全流程(附完整工程代码)

STM32F103C8T6硬件SPI驱动W25Q64 Flash全流程实战指南

在嵌入式开发中,外部Flash存储器的使用是一个常见需求。W25Q64作为一款性价比极高的8MB SPI Flash芯片,被广泛应用于各种嵌入式系统中。本文将手把手带你完成从零开始搭建STM32F103C8T6(蓝桥杯开发板常用MCU)硬件SPI驱动W25Q64的全过程,包括工程搭建、硬件连接、底层驱动编写以及数据验证。

1. 硬件准备与连接

在开始编码前,我们需要确保硬件连接正确。STM32F103C8T6与W25Q64的连接方式如下:

STM32引脚W25Q64引脚功能说明
PA4CS片选信号
PA5CLK时钟信号
PA6DO/MISO主机输入从机输出
PA7DI/MOSI主机输出从机输入
3.3VVCC电源正极
GNDGND电源地

注意:W25Q64的工作电压为2.7V-3.6V,务必连接到3.3V电源,不可接5V,否则会损坏芯片。

硬件连接时还需要注意以下几点:

  • 确保所有GND连接在一起
  • 信号线长度尽可能短
  • 如果布线较长,可考虑在SCK线上串联22Ω电阻减少振铃
  • 在VCC和GND之间放置0.1μF去耦电容

2. 工程环境搭建

首先我们需要创建一个基本的STM32工程。这里以Keil MDK为例:

  1. 打开Keil uVision,选择Project → New μVision Project
  2. 选择保存路径并命名工程(如"W25Q64_SPI_Demo")
  3. 设备选择STMicroelectronics → STM32F103C8
  4. 在Manage Run-Time Environment中勾选:
    • CMSIS → CORE
    • Device → Startup
    • STM32Cube Framework → CMSIS → STM32F1xx
  5. 点击OK创建工程

接下来添加必要的驱动文件:

  • 在工程中新建Hardware/SPI和Hardware/W25Q64文件夹
  • 分别创建spi.h/spi.c和w25q64.h/w25q64.c文件
  • 配置工程包含路径指向这些文件夹

3. 硬件SPI初始化配置

硬件SPI的初始化是驱动W25Q64的关键。我们需要正确配置SPI的工作模式和参数:

// spi.h #ifndef __SPI_H__ #define __SPI_H__ #include "stm32f10x.h" void SPI1_Init(void); void SPI1_Start(void); void SPI1_Stop(void); uint8_t SPI1_ReadWriteByte(uint8_t TxData); #endif
// spi.c #include "spi.h" #include "stm32f10x_gpio.h" #include "stm32f10x_spi.h" // SPI1引脚初始化 void SPI1_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // PA4(CS) 推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // PA5(SCK), PA7(MOSI) 复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); // PA6(MISO) 浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // 默认CS高电平 GPIO_SetBits(GPIOA, GPIO_Pin_4); } // SPI1模式配置 void SPI1_Mode_Init(void) { SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); } void SPI1_Init(void) { SPI1_GPIO_Init(); SPI1_Mode_Init(); } void SPI1_Start(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_4); // CS拉低 } void SPI1_Stop(void) { GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS拉高 } uint8_t SPI1_ReadWriteByte(uint8_t TxData) { while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, TxData); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); return SPI_I2S_ReceiveData(SPI1); }

关键配置参数说明:

  • SPI模式:全双工主模式
  • 数据大小:8位
  • 时钟极性(CPOL):低电平
  • 时钟相位(CPHA):第一个边沿采样
  • 波特率预分频:4分频(系统时钟72MHz时,SPI时钟为18MHz)
  • 数据顺序:MSB优先

4. W25Q64驱动实现

W25Q64的驱动需要实现基本的读写擦除功能。首先定义W25Q64的指令集:

// w25q64.h #ifndef __W25Q64_H__ #define __W25Q64_H__ #include "stm32f10x.h" // W25Q64指令集 #define W25Q64_WriteEnable 0x06 #define W25Q64_WriteDisable 0x04 #define W25Q64_ReadStatusReg1 0x05 #define W25Q64_ReadStatusReg2 0x35 #define W25Q64_WriteStatusReg 0x01 #define W25Q64_PageProgram 0x02 #define W25Q64_SectorErase 0x20 #define W25Q64_BlockErase32K 0x52 #define W25Q64_BlockErase64K 0xD8 #define W25Q64_ChipErase 0xC7 #define W25Q64_PowerDown 0xB9 #define W25Q64_ReleasePowerDown 0xAB #define W25Q64_ManufactDeviceID 0x90 #define W25Q64_ReadUniqueID 0x4B #define W25Q64_JedecDeviceID 0x9F #define W25Q64_ReadData 0x03 #define W25Q64_FastRead 0x0B #define W25Q64_DummyByte 0xFF // 函数声明 void W25Q64_Init(void); void W25Q64_ReadID(uint8_t *ManufacturerID, uint16_t *DeviceID); void W25Q64_WriteEnable(void); void W25Q64_WaitBusy(void); void W25Q64_SectorErase(uint32_t SectorAddr); void W25Q64_PageWrite(uint32_t Addr, uint8_t *pBuffer, uint16_t NumByteToWrite); void W25Q64_ReadData(uint32_t Addr, uint8_t *pBuffer, uint16_t NumByteToRead); #endif

接下来实现具体的驱动函数:

// w25q64.c #include "w25q64.h" #include "spi.h" #include "delay.h" // 初始化W25Q64 void W25Q64_Init(void) { SPI1_Init(); } // 读取制造商ID和设备ID void W25Q64_ReadID(uint8_t *ManufacturerID, uint16_t *DeviceID) { SPI1_Start(); SPI1_ReadWriteByte(W25Q64_JedecDeviceID); *ManufacturerID = SPI1_ReadWriteByte(W25Q64_DummyByte); *DeviceID = SPI1_ReadWriteByte(W25Q64_DummyByte); *DeviceID <<= 8; *DeviceID |= SPI1_ReadWriteByte(W25Q64_DummyByte); SPI1_Stop(); } // 写使能 void W25Q64_WriteEnable(void) { SPI1_Start(); SPI1_ReadWriteByte(W25Q64_WriteEnable); SPI1_Stop(); } // 等待忙状态结束 void W25Q64_WaitBusy(void) { uint32_t timeout = 500000; // 超时计数 SPI1_Start(); SPI1_ReadWriteByte(W25Q64_ReadStatusReg1); while((SPI1_ReadWriteByte(W25Q64_DummyByte) & 0x01) == 0x01) { if(timeout-- == 0) break; } SPI1_Stop(); } // 扇区擦除(4KB) void W25Q64_SectorErase(uint32_t SectorAddr) { W25Q64_WriteEnable(); SPI1_Start(); SPI1_ReadWriteByte(W25Q64_SectorErase); SPI1_ReadWriteByte((SectorAddr >> 16) & 0xFF); SPI1_ReadWriteByte((SectorAddr >> 8) & 0xFF); SPI1_ReadWriteByte(SectorAddr & 0xFF); SPI1_Stop(); W25Q64_WaitBusy(); } // 页编程(最大256字节) void W25Q64_PageWrite(uint32_t Addr, uint8_t *pBuffer, uint16_t NumByteToWrite) { uint16_t i; W25Q64_WriteEnable(); SPI1_Start(); SPI1_ReadWriteByte(W25Q64_PageProgram); SPI1_ReadWriteByte((Addr >> 16) & 0xFF); SPI1_ReadWriteByte((Addr >> 8) & 0xFF); SPI1_ReadWriteByte(Addr & 0xFF); for(i=0; i<NumByteToWrite; i++) { SPI1_ReadWriteByte(pBuffer[i]); } SPI1_Stop(); W25Q64_WaitBusy(); } // 读取数据 void W25Q64_ReadData(uint32_t Addr, uint8_t *pBuffer, uint16_t NumByteToRead) { uint16_t i; SPI1_Start(); SPI1_ReadWriteByte(W25Q64_ReadData); SPI1_ReadWriteByte((Addr >> 16) & 0xFF); SPI1_ReadWriteByte((Addr >> 8) & 0xFF); SPI1_ReadWriteByte(Addr & 0xFF); for(i=0; i<NumByteToRead; i++) { pBuffer[i] = SPI1_ReadWriteByte(W25Q64_DummyByte); } SPI1_Stop(); }

5. 功能测试与验证

完成驱动编写后,我们需要测试W25Q64的各项功能。这里我们使用OLED显示屏来显示测试结果:

// main.c #include "stm32f10x.h" #include "delay.h" #include "oled.h" #include "w25q64.h" int main(void) { uint8_t ManufacturerID; uint16_t DeviceID; uint8_t WriteBuffer[256]; uint8_t ReadBuffer[256]; uint16_t i; // 初始化 Delay_Init(); OLED_Init(); W25Q64_Init(); // 显示标题 OLED_ShowString(1, 1, "W25Q64 Test"); // 读取ID并显示 W25Q64_ReadID(&ManufacturerID, &DeviceID); OLED_ShowString(2, 1, "MID:0x"); OLED_ShowHexNum(2, 7, ManufacturerID, 2); OLED_ShowString(3, 1, "DID:0x"); OLED_ShowHexNum(3, 7, DeviceID, 4); // 准备测试数据 for(i=0; i<256; i++) { WriteBuffer[i] = i; } // 擦除扇区0 OLED_ShowString(4, 1, "Erasing Sector 0..."); W25Q64_SectorErase(0x000000); OLED_ShowString(4, 1, "Erase Complete! "); // 写入数据 OLED_ShowString(5, 1, "Writing Data... "); W25Q64_PageWrite(0x000000, WriteBuffer, 256); OLED_ShowString(5, 1, "Write Complete! "); // 读取数据 OLED_ShowString(6, 1, "Reading Data... "); W25Q64_ReadData(0x000000, ReadBuffer, 256); OLED_ShowString(6, 1, "Read Complete! "); // 验证数据 for(i=0; i<256; i++) { if(WriteBuffer[i] != ReadBuffer[i]) { OLED_ShowString(7, 1, "Verify Failed! "); break; } } if(i == 256) { OLED_ShowString(7, 1, "Verify Success! "); } while(1) { } }

测试流程说明:

  1. 初始化系统时钟、延时函数、OLED和W25Q64
  2. 读取W25Q64的制造商ID和设备ID并显示
  3. 准备256字节的测试数据(0x00-0xFF)
  4. 擦除扇区0(地址0x000000开始的4KB)
  5. 将测试数据写入扇区0
  6. 从扇区0读取数据
  7. 比较写入和读取的数据是否一致
  8. 显示验证结果

6. 性能优化与高级功能

在基本功能实现后,我们可以进一步优化性能和实现更高级的功能:

6.1 SPI时钟速度优化

W25Q64支持最高80MHz的SPI时钟。我们可以调整预分频值来提高传输速率:

// 修改SPI初始化中的预分频值 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; // 36MHz

注意:提高SPI时钟速度需要考虑信号完整性和PCB布线质量。如果出现通信错误,可以适当降低时钟速度或改善硬件设计。

6.2 多扇区连续写入

W25Q64的页编程操作每次最多写入256字节。要实现更大数据量的连续写入,可以使用以下方法:

void W25Q64_MultiPageWrite(uint32_t Addr, uint8_t *pBuffer, uint32_t NumByteToWrite) { uint32_t PageRemain; uint32_t WriteLen; uint32_t i; for(i=0; i<NumByteToWrite; ) { PageRemain = 256 - (Addr % 256); // 当前页剩余空间 WriteLen = (NumByteToWrite - i) > PageRemain ? PageRemain : (NumByteToWrite - i); W25Q64_PageWrite(Addr, pBuffer + i, WriteLen); Addr += WriteLen; i += WriteLen; } }

6.3 快速读取模式

W25Q64支持快速读取模式(Fast Read),相比普通读取模式可以提高读取速度:

void W25Q64_FastReadData(uint32_t Addr, uint8_t *pBuffer, uint16_t NumByteToRead) { uint16_t i; SPI1_Start(); SPI1_ReadWriteByte(W25Q64_FastRead); SPI1_ReadWriteByte((Addr >> 16) & 0xFF); SPI1_ReadWriteByte((Addr >> 8) & 0xFF); SPI1_ReadWriteByte(Addr & 0xFF); SPI1_ReadWriteByte(W25Q64_DummyByte); // 快速读取需要一个dummy字节 for(i=0; i<NumByteToRead; i++) { pBuffer[i] = SPI1_ReadWriteByte(W25Q64_DummyByte); } SPI1_Stop(); }

6.4 写保护功能

W25Q64提供了写保护功能,可以防止意外修改数据:

// 启用写保护 void W25Q64_WriteProtect(void) { SPI1_Start(); SPI1_ReadWriteByte(W25Q64_WriteDisable); SPI1_Stop(); } // 检查写保护状态 uint8_t W25Q64_IsWriteProtected(void) { uint8_t status; SPI1_Start(); SPI1_ReadWriteByte(W25Q64_ReadStatusReg1); status = SPI1_ReadWriteByte(W25Q64_DummyByte); SPI1_Stop(); return (status & 0x02) ? 1 : 0; }

7. 常见问题与解决方案

在实际开发中,可能会遇到各种问题。以下是一些常见问题及其解决方法:

7.1 无法读取设备ID

现象:读取的制造商ID或设备ID不正确或全为0xFF。

可能原因及解决方法

  1. 硬件连接错误
    • 检查所有连线是否正确,特别是CS、SCK、MOSI、MISO
    • 确认电源电压为3.3V
  2. SPI配置错误
    • 确认SPI模式设置为模式0或模式3(CPOL=0, CPHA=0或CPOL=1, CPHA=1)
    • 检查SPI时钟速度是否过高,可尝试降低预分频值
  3. 芯片未正确复位
    • 尝试重新上电
    • 发送释放掉电指令(0xAB)

7.2 写入数据后读取不正确

现象:写入数据后读取的数据与写入的不一致。

可能原因及解决方法

  1. 未先擦除扇区
    • Flash存储器必须先擦除才能写入,确保在写入前执行了扇区擦除
  2. 跨页写入未处理
    • 单次写入不能跨页(256字节边界),需要分多次写入
  3. 写入后未等待操作完成
    • 在写入操作后调用W25Q64_WaitBusy()等待操作完成

7.3 擦除或写入操作失败

现象:擦除或写入操作没有效果。

可能原因及解决方法

  1. 未发送写使能指令
    • 在执行擦除或写入操作前必须先发送写使能指令(0x06)
  2. 写保护启用
    • 检查状态寄存器的写保护位,必要时禁用写保护
  3. 电压不稳定
    • 检查电源电压是否稳定,在VCC和GND之间添加更大的滤波电容

7.4 长时间使用后数据丢失

现象:存储的数据在一段时间后丢失或损坏。

可能原因及解决方法

  1. 擦写次数超过限制
    • W25Q64每个扇区的擦写寿命约为10万次,需优化算法减少擦写频率
  2. 电源干扰
    • 加强电源滤波,确保在写入操作期间电源稳定
  3. 环境温度过高
    • 避免在高温环境下长期工作,必要时降低SPI时钟速度

8. 工程代码结构优化

为了使工程更加模块化和易于维护,我们可以优化代码结构如下:

W25Q64_SPI_Demo/ ├── CMSIS/ # CMSIS核心文件 ├── STM32F10x_StdPeriph_Driver/ # 标准外设驱动 ├── User/ │ ├── main.c # 主程序 │ ├── delay.c # 延时函数 │ ├── oled.c # OLED驱动 │ ├── spi.c # SPI驱动 │ ├── w25q64.c # W25Q64驱动 │ ├── inc/ # 头文件目录 │ │ ├── delay.h │ │ ├── oled.h │ │ ├── spi.h │ │ └── w25q64.h │ └── src/ # 源文件目录 └── MDK-ARM/ # Keil工程文件

在Keil中设置包含路径时,确保包含以下路径:

  • User/inc
  • CMSIS
  • STM32F10x_StdPeriph_Driver/inc

9. 实际应用案例

W25Q64在实际项目中有广泛的应用场景,下面介绍几个典型应用:

9.1 固件存储与升级

W25Q64可以用于存储设备固件,实现IAP(In Application Programming)功能:

// 固件更新函数 void Firmware_Update(uint8_t *NewFirmware, uint32_t FirmwareSize) { uint32_t i; uint32_t SectorAddr; uint32_t SectorCount = (FirmwareSize + 4095) / 4096; // 计算需要的扇区数 // 擦除必要的扇区 for(i=0; i<SectorCount; i++) { SectorAddr = FIRMWARE_START_ADDR + i * 4096; W25Q64_SectorErase(SectorAddr); } // 写入新固件 W25Q64_MultiPageWrite(FIRMWARE_START_ADDR, NewFirmware, FirmwareSize); // 验证固件 // ... // 更新成功,设置标志准备重启 // ... }

9.2 数据日志存储

在需要记录设备运行数据的应用中,W25Q64可以作为数据存储器:

// 日志数据结构 typedef struct { uint32_t timestamp; float temperature; float humidity; uint16_t pressure; } LogEntry; // 写入日志 void Write_Log(LogEntry *entry) { static uint32_t logAddr = LOG_START_ADDR; // 检查是否需要擦除新扇区 if((logAddr % 4096) == 0) { W25Q64_SectorErase(logAddr); } // 写入日志条目 W25Q64_PageWrite(logAddr, (uint8_t *)entry, sizeof(LogEntry)); // 更新写入地址 logAddr += sizeof(LogEntry); // 处理循环写入 if(logAddr >= LOG_END_ADDR) { logAddr = LOG_START_ADDR; } }

9.3 字库存储与显示

在需要显示多种语言的设备中,W25Q64可以存储字库数据:

// 从Flash读取字模数据 void Get_FontData(uint16_t fontCode, uint8_t *buffer) { uint32_t addr = FONT_START_ADDR + fontCode * FONT_SIZE; W25Q64_ReadData(addr, buffer, FONT_SIZE); } // 显示字符 void Show_Char(uint16_t x, uint16_t y, uint16_t fontCode) { uint8_t fontData[FONT_SIZE]; Get_FontData(fontCode, fontData); OLED_ShowFont(x, y, fontData); }

10. 扩展思考与进阶方向

掌握了W25Q64的基本驱动后,可以进一步探索以下方向:

10.1 文件系统集成

在W25Q64上实现文件系统(如FATFS、LittleFS等)可以更方便地管理存储数据:

// 初始化FATFS文件系统 FATFS fs; FIL file; FRESULT res; res = f_mount(&fs, "0:", 1); // 挂载文件系统 if(res == FR_OK) { res = f_open(&file, "0:/data.log", FA_READ | FA_WRITE | FA_OPEN_ALWAYS); if(res == FR_OK) { // 文件操作... f_close(&file); } f_mount(NULL, "0:", 0); // 卸载文件系统 }

10.2 双SPI/四SPI模式

W25Q64支持双SPI和四SPI模式,可以进一步提高数据传输速率:

// 切换到四SPI模式 void W25Q64_EnableQuadMode(void) { uint8_t status; // 读取状态寄存器2 SPI1_Start(); SPI1_ReadWriteByte(W25Q64_ReadStatusReg2); status = SPI1_ReadWriteByte(W25Q64_DummyByte); SPI1_Stop(); // 设置QE位 if((status & 0x02) == 0) { W25Q64_WriteEnable(); SPI1_Start(); SPI1_ReadWriteByte(W25Q64_WriteStatusReg); SPI1_ReadWriteByte(0x00); // 状态寄存器1保持原值 SPI1_ReadWriteByte(0x02); // 设置状态寄存器2的QE位 SPI1_Stop(); W25Q64_WaitBusy(); } }

10.3 磨损均衡算法

为了延长Flash寿命,可以实现简单的磨损均衡算法:

// 简单的磨损均衡实现 uint32_t Get_NextWriteAddr(void) { static uint32_t currentAddr = DATA_START_ADDR; static uint16_t writeCount = 0; uint32_t nextAddr; nextAddr = currentAddr; currentAddr += DATA_BLOCK_SIZE; // 检查是否需要擦除新区块 if((currentAddr % 4096) == 0) { W25Q64_SectorErase(currentAddr); } // 处理循环写入 if(currentAddr >= DATA_END_ADDR) { currentAddr = DATA_START_ADDR; writeCount++; // 每循环一定次数后移动起始地址实现简单均衡 if(writeCount % WEAR_LEVELING_FACTOR == 0) { currentAddr += 4096; // 移动一个扇区 if(currentAddr >= DATA_END_ADDR) { currentAddr = DATA_START_ADDR; } } } return nextAddr; }

10.4 掉电保护机制

在关键数据写入时实现掉电保护:

// 安全写入函数 uint8_t Safe_Write(uint32_t addr, uint8_t *data, uint16_t len) { uint8_t status = 0; uint32_t crc = Calculate_CRC(data, len); // 写入数据 W25Q64_PageWrite(addr, data, len); // 写入CRC校验值 W25Q64_PageWrite(addr + len, (uint8_t *)&crc, 4); // 验证写入 uint8_t verifyBuf[len]; uint32_t verifyCrc; W25Q64_ReadData(addr, verifyBuf, len); W25Q64_ReadData(addr + len, (uint8_t *)&verifyCrc, 4); if(memcmp(data, verifyBuf, len) == 0 && crc == verifyCrc) { status = 1; // 写入成功 } return status; }

通过本文的详细讲解,你应该已经掌握了STM32F103C8T6硬件SPI驱动W25Q64 Flash的全流程。从硬件连接到软件实现,从基础功能到高级应用,这些知识将帮助你在实际项目中灵活运用SPI Flash存储器。

http://www.jsqmd.com/news/767814/

相关文章:

  • C#基础(持续更新中)
  • Python初学者项目练习9--对简单列表元素排序
  • 解决ZYNQ裸机网络扩展难题:为LWIP库添加自定义PHY驱动与SDK配置界面
  • Windows系统光标深度替换:INF方案实现macOS指针移植与优化
  • AI编码助手统一配置工具agent-dotfiles:告别重复配置,实现规则与技能一键同步
  • BrowserClaw:基于Puppeteer与Playwright的浏览器自动化与数据抓取实践
  • AI工具搭建自动化视频生成图像缩放
  • ChatGPT文档格式化指令:打造Google Docs无缝协作的AI写作规范
  • GRADFILTERING:基于梯度信噪比的指令调优数据筛选方法
  • 别再死记硬背async/await了!用Playwright+Python写自动化脚本,这3个坑我帮你踩过了
  • 千问 LeetCode 2127.参加会议的最多员工数 public int maximumInvitations(int[] favorite)
  • 解释器模式是行为型设计模式的一种,其核心思想是给定一个语言,定义它的文法的一种表示
  • STM32G431RBT6的HAL库避坑指南:蓝桥杯嵌入式那些CubeMX没告诉你的细节
  • 构建本地化音视频转录分析平台:Whisper+Ollama+Meilisearch实战
  • SolidGPT实战指南:基于语义搜索的代码与文档智能问答系统
  • 避坑指南:SAP固定资产配置里,记账码70和31千万别乱选!附SPRO完整路径
  • 想在Win10任务栏显示秒数?试试用StartAllBack配合注册表修改(附详细步骤)
  • 【Redis】Redis——过期键删除策略、内存淘汰8种策略、LRU/LFU实现
  • 秒级推演赋能复杂场景,镜像视界夯实工业数字根基
  • SpringBoot + Thymeleaf 实战:手把手教你从零搭建一个婚纱租赁网站(附完整源码)
  • PageIndex:基于RAG的网页智能知识库构建实战指南
  • HoRain云--超全PHP安装指南:Linux/Windows/macOS全攻略
  • MQTTX与AI助手实时交互:基于MCP与SSE的物联网协议桥接实践
  • 基于Dev Containers的标准化开发环境构建与实战指南
  • STM32定时器OPM单脉冲模式实战:从驱动蜂鸣器到生成精准PWM脉冲(以TIM4为例)
  • synchronized内存布局图(bit 精确位置)
  • Promptr:用自然语言指令自动化重构代码的AI工具实践指南
  • 在github上快速部署taotoken的python调用示例
  • 千问 LeetCode 2127.参加会议的最多员工数 Python3实现
  • AI智能体全栈开发框架解析:从核心架构到生产部署