H743双存储设备模拟U盘全攻略STM32CubeMX配置与文件系统安全处理最近在做一个基于STM32H743的工业数据采集终端项目需要将采集到的数据同时存储到本地SPI Flash和SD卡中并且能通过USB接口快速导出。听起来是个挺常见的需求对吧但当我真正动手实现时才发现这里面藏着不少“坑”——特别是当你想让电脑同时识别这两个存储设备为U盘时。最让我头疼的是文件系统的安全问题。想象一下设备正在往SPI Flash里写入关键数据这时候用户突然插上USB线电脑弹出了两个“U盘”。如果处理不当轻则文件损坏重则整个文件系统崩溃数据全丢。我花了整整两周时间调试才找到了一套相对稳妥的解决方案。这篇文章就是把我踩过的坑、试过的方案、最终有效的配置方法都整理出来。如果你也在用H743做类似的双存储设备模拟U盘项目特别是对数据完整性要求比较高的场景相信这些经验能帮你少走很多弯路。1. 理解H743双存储模拟U盘的架构挑战在开始配置之前我们得先搞清楚H743同时管理SPI Flash和SD卡作为USB大容量存储设备MSC时底层到底发生了什么。这不仅仅是简单的“112”的问题。1.1 硬件资源与USB MSC的映射关系STM32H743的USB OTG FS/HS接口支持大容量存储类MSC协议但协议本身是为单个逻辑单元LUN设计的。当我们想要同时暴露两个物理存储设备时实际上是在USB协议层做了个“虚拟化”——把两个独立的存储介质映射为同一个USB设备下的两个逻辑单元。这里有个关键概念逻辑单元号LUN。每个LUN在电脑上会显示为一个独立的磁盘驱动器。对于我们的双存储方案通常LUN 0映射到SD卡LUN 1映射到SPI Flash但问题来了这两个存储介质的物理特性完全不同。特性SD卡通常LUN 0SPI Flash通常LUN 1块大小512字节标准可配置但需与MSC对齐访问速度较快依赖SDMMC时钟较慢受SPI时钟限制写入方式块写入有缓存页写入需擦除操作文件系统FAT32/exFAT常见通常为FATFS over SPI热插拔支持物理上支持逻辑上模拟这种差异导致了第一个大坑块大小不匹配。很多老教程特别是基于F4系列的会告诉你SPI Flash的块大小可以设为4096字节然后在CubeMX里把MSC的块大小也设为4096。但在H743上这套行不通了——电脑根本识别不出来。我试了好几次最后发现必须统一使用512字节的块大小。即使你的SPI Flash物理页大小是4096字节在MSC层也要按512字节来模拟。这是CubeMX更新后带来的变化也可能是USB主机控制器电脑对MSC设备的兼容性要求更严格了。1.2 文件系统并发访问的隐患当你的嵌入式程序正在通过FATFS往SPI Flash里写文件时如果用户突然插入USB电脑端的文件系统驱动会立即尝试读取这个“U盘”的目录结构。这时候就出现了并发访问冲突。具体来说问题通常出现在这几个环节FAT表更新冲突FATFS在写入文件时会先更新FAT表再写入数据。如果这时候USB主机也在读取FAT表可能读到半更新的状态。目录项不一致文件创建或删除时目录项会被修改。并发访问可能导致目录项损坏。缓存未同步嵌入式端可能有写缓存数据还没真正落到Flash上但USB主机已经读取了“看似完整”的数据。有趣的是我在测试中发现SD卡很少出现这个问题而SPI Flash几乎每次都会中招。后来分析原因可能是SD卡控制器内部有更好的事务管理机制而SPI Flash的访问更“原始”缺乏并发保护。注意这不是说SD卡就绝对安全。在极端情况下如果SD卡正在执行多块写入操作USB插入同样可能导致问题只是概率比SPI Flash低得多。2. STM32CubeMX的精准配置步骤好了理解了背后的原理我们来看看具体的配置。我建议你按照这个顺序来避免走弯路。2.1 基础外设配置首先确保你的SDMMC和SPI外设已经正确配置并能正常工作。这不是本文的重点但有几个关键点需要检查SDMMC配置以SD卡为例时钟分频要合理H743的SDMMC时钟最高可达200MHz但实际要根据你的SD卡等级来设置总线宽度建议用4位数据线提升速度别忘了使能DMA否则大量数据传输时会占用大量CPUSPI Flash配置SPI模式用Mode 0或Mode 3看你的Flash芯片手册时钟频率别太高很多SPI Flash最高也就50MHz记得配置好GPIO特别是片选引脚2.2 USB MSC核心配置现在进入关键部分。在CubeMX的Middleware选项卡中找到USB_DEVICE选择Mass Storage Class。这里有几个容易出错的配置项/* 在usbd_conf.h中需要注意的配置 */ #define MSC_MEDIA_PACKET 512 /* 必须是512不要改 */ /* 在usbd_storage_if.c中 */ #define STORAGE_LUN_NBR 2 /* 两个LUNSD卡和SPI Flash */ #define STORAGE_BLK_SIZ 512 /* 块大小固定512字节 */为什么必须是512字节我最初也不理解直到看了USB MSC协议规范。大多数Windows和macOS的USB大容量存储驱动都假设设备的块大小是512字节这是从传统硬盘继承来的“约定俗成”。虽然协议理论上支持其他块大小但很多主机实现只认512字节。如果你设成4096可能会出现以下几种情况电脑完全不识别设备电脑识别了但显示容量错误能识别但无法格式化格式化后写入数据立即损坏所以老老实实用512字节这是血泪教训。2.3 双LUN的识别信息配置电脑需要知道每个LUN对应什么设备。这通过Inquiry Data来实现。在usbd_storage_if.c中你需要修改STORAGE_Inquirydata_FS数组const int8_t STORAGE_Inquirydata_FS[] { /* LUN 0 - SD卡 */ 0x00, 0x80, 0x02, 0x02, 0x1F, 0x00, 0x00, 0x00, /* 标准头 */ S, T, M, 3, 2, , , , /* 厂商名STM32 */ S, D, _, C, a, r, d, , /* 产品名SD_Card */ , , , , , , , , /* 产品名续 */ 1, ., 0, 0, /* 版本1.00 */ /* LUN 1 - SPI Flash */ 0x00, 0x80, 0x02, 0x02, 0x1F, 0x00, 0x00, 0x00, /* 标准头 */ S, T, M, 3, 2, , , , /* 厂商名STM32 */ S, P, I, _, F, l, a, s, h, /* 产品名SPI_Flash */ , , , , , , , /* 产品名续 */ 1, ., 0, 0 /* 版本1.00 */ };这个数组的每个字节都有特定含义字节0外设类型0x00表示直接访问块设备字节1RMB标志0x80表示可移动字节2-3协议版本字节4数据长度0x1F表示31字节后还有数据字节8-158字节厂商字符串字节16-3116字节产品字符串字节32-354字节版本字符串提示厂商和产品字符串最好用ASCII字符填满固定长度不足的用空格补足。有些老版本的Windows会严格检查这些字段的长度。3. 文件系统安全处理机制这是本文最核心的部分。如何保证在USB随时可能插入的情况下文件系统不会崩溃3.1 检测USB连接状态首先我们需要知道USB什么时候被插上了。H743的USB设备库提供了回调函数但默认可能没启用。你需要自己添加连接状态检测/* 在usbd_conf.h中添加 */ #define USER_USB_DEVICE_CONNECT_CALLBACK 1 /* 在usbd_custom_hid_if.c或类似文件中 */ extern __IO uint8_t usb_connected; void HAL_PCD_ConnectCallback(PCD_HandleTypeDef *hpcd) { usb_connected 1; /* 可以在这里设置一个标志通知主程序 */ } void HAL_PCD_DisconnectCallback(PCD_HandleTypeDef *hpcd) { usb_connected 0; }但仅仅知道连接状态还不够。USB插入后主机不会立即访问存储设备通常会有一个延迟。我们可以利用这个时间窗口做安全准备。3.2 实现安全的写入锁机制我设计了一个三级保护机制在实际项目中验证效果不错第一级软件写标志在开始任何文件系统写入操作前先检查一个全局标志volatile uint8_t fs_write_in_progress 0; volatile uint8_t usb_connection_pending 0; int safe_fs_write_begin(void) { if (usb_connection_pending) { return -1; /* USB即将连接拒绝写入 */ } if (fs_write_in_progress) { return -2; /* 已有写入在进行 */ } fs_write_in_progress 1; return 0; } void safe_fs_write_end(void) { fs_write_in_progress 0; }第二级硬件写保护引脚如果你的设计有额外的GPIO可用可以接一个写保护信号到SPI Flash的WP引脚。在检测到USB插入时立即拉低这个引脚物理上禁止写入void enable_flash_write_protection(void) { HAL_GPIO_WritePin(FLASH_WP_GPIO_Port, FLASH_WP_Pin, GPIO_PIN_RESET); } void disable_flash_write_protection(void) { HAL_GPIO_WritePin(FLASH_WP_GPIO_Port, FLASH_WP_Pin, GPIO_PIN_SET); }第三级事务完整性检查对于关键数据实现一个简单的事务机制typedef struct { uint32_t magic_start; /* 魔术字如0x55AA55AA */ uint8_t data[512]; /* 实际数据 */ uint32_t checksum; /* 校验和 */ uint32_t magic_end; /* 魔术字如0xAA55AA55 */ } safe_transaction_t; int write_with_transaction(uint32_t addr, uint8_t *data, uint32_t len) { safe_transaction_t trans; trans.magic_start 0x55AA55AA; memcpy(trans.data, data, len 512 ? 512 : len); trans.checksum calculate_checksum(data, len); trans.magic_end 0xAA55AA55; /* 原子性写入如果Flash支持 */ return SPI_FLASH_Write((uint8_t*)trans, addr, sizeof(trans)); }3.3 处理USB插入时的紧急情况即使有预防措施USB还是可能在写入过程中插入。这时候我们需要一个“紧急刹车”机制void USB_Connect_IRQHandler(void) { /* 立即标记USB连接待处理 */ usb_connection_pending 1; /* 检查是否有进行中的写入 */ if (fs_write_in_progress) { /* 尝试优雅结束当前写入 */ if (current_write_can_abort()) { abort_current_write(); fs_write_in_progress 0; } else { /* 不能中止的写入等待完成但设置超时 */ uint32_t timeout HAL_GetTick() 100; /* 最多等100ms */ while (fs_write_in_progress (HAL_GetTick() timeout)) { /* 空循环等待实际项目中可以加入一些低优先级任务 */ } if (fs_write_in_progress) { /* 超时了强制结束并标记文件系统需要修复 */ force_end_write(); mark_fs_needs_recovery(); } } } /* 现在可以安全地让USB枚举设备了 */ usb_connected 1; usb_connection_pending 0; }这个机制的关键是平衡既要尽快响应USB连接又要给文件系统足够的时间完成关键操作。100ms的超时是我通过实验找到的合理值——对于大多数SPI Flash的页写入操作来说足够了用户也不会感觉到明显的延迟。4. 存储接口层的具体实现现在我们来深入看看usbd_storage_if.c这个关键文件的具体实现。原始代码只提供了骨架我们需要填充血肉。4.1 初始化与容量报告STORAGE_Init_FS函数在USB枚举时被调用用于初始化存储设备。但要注意这个函数调用时USB主机可能还没有开始真正的数据传输所以这里不适合做耗时的初始化int8_t STORAGE_Init_FS(uint8_t lun) { /* 简单返回成功即可真正的初始化应该在别处完成 */ return (USBD_OK); }更重要的是STORAGE_GetCapacity_FS函数它告诉主机每个LUN有多大容量int8_t STORAGE_GetCapacity_FS(uint8_t lun, uint32_t *block_num, uint16_t *block_size) { if (lun 0) { /* SD卡 */ *block_num hsd1.SdCard.BlockNbr; /* 总块数 */ *block_size 512; /* 固定512字节 */ } else if (lun 1) { /* SPI Flash */ *block_num SPI_FLASH_TOTAL_SIZE / 512; /* 计算块数 */ *block_size 512; /* 固定512字节 */ /* 重要确保Flash容量是512的整数倍 */ if ((SPI_FLASH_TOTAL_SIZE % 512) ! 0) { /* 如果不是整数倍报告稍小的容量 */ *block_num (SPI_FLASH_TOTAL_SIZE / 512) * 512; } } else { return USBD_FAIL; /* 无效的LUN */ } return USBD_OK; }这里有个细节SPI Flash的总容量可能不是512字节的整数倍。比如一颗8MB的Flash实际是8388608字节除以512是16384块正好整除。但如果是16MB的Flash16777216字节除以512是32768块也整除。大多数常见的Flash容量都是512的整数倍但如果你用的芯片不是就需要像上面那样处理报告一个稍小的容量避免访问越界。4.2 读写操作的实现与优化读写函数是性能的关键。先看读操作int8_t STORAGE_Read_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len) { if (lun 0) { /* SD卡读取 */ HAL_StatusTypeDef status; /* 检查SD卡是否就绪 */ if (HAL_SD_GetCardState(hsd1) ! HAL_SD_CARD_TRANSFER) { return USBD_FAIL; } /* 使用DMA读取多个块 */ status HAL_SD_ReadBlocks_DMA(hsd1, buf, blk_addr, blk_len); if (status ! HAL_OK) { return USBD_FAIL; } /* 等待DMA完成 */ while (HAL_SD_GetCardState(hsd1) HAL_SD_CARD_TRANSFER) { /* 可以在这里加入超时检测 */ } } else if (lun 1) { /* SPI Flash读取 */ uint32_t start_addr blk_addr * 512; uint32_t length blk_len * 512; /* 检查是否在写入保护期间 */ if (flash_write_protected (start_addr current_write_area_start || start_addr current_write_area_end)) { /* 允许读取非写入区域 */ SPI_FLASH_Read(buf, start_addr, length); } else if (flash_write_protected) { /* 尝试读取正在写入的区域返回错误或缓存数据 */ return read_from_write_cache(buf, start_addr, length); } else { /* 正常读取 */ SPI_FLASH_Read(buf, start_addr, length); } } return USBD_OK; }对于SPI Flash的读取我加入了写保护检查。如果某个区域正在被嵌入式程序写入而USB主机又试图读取这个区域我们有几个选择直接返回错误USBD_FAIL返回上一次成功写入的数据如果有缓存等待写入完成可能超时在实际项目中我选择了方案2因为返回错误可能导致电脑弹出“磁盘错误”的警告用户体验不好。写操作更复杂因为要处理并发问题int8_t STORAGE_Write_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len) { /* 首先检查是否允许写入 */ if (safe_fs_write_begin() ! 0) { return USBD_FAIL; } if (lun 0) { /* SD卡写入 */ HAL_StatusTypeDef status; /* SD卡通常可以安全写入但仍要检查状态 */ if (HAL_SD_GetCardState(hsd1) ! HAL_SD_CARD_TRANSFER) { safe_fs_write_end(); return USBD_FAIL; } status HAL_SD_WriteBlocks_DMA(hsd1, (uint8_t *)buf, blk_addr, blk_len); if (status ! HAL_OK) { safe_fs_write_end(); return USBD_FAIL; } /* 等待写入完成 */ while (HAL_SD_GetCardState(hsd1) HAL_SD_CARD_TRANSFER) { if ((HAL_GetTick() - start_time) 5000) { /* 5秒超时 */ safe_fs_write_end(); return USBD_FAIL; } } } else if (lun 1) { /* SPI Flash写入 - 需要特别小心 */ uint32_t start_addr blk_addr * 512; uint32_t length blk_len * 512; /* 检查地址对齐对于需要擦除的Flash很重要 */ if ((start_addr % SPI_FLASH_SECTOR_SIZE) ! 0) { /* 需要先读取-修改-写入 */ if (handle_unaligned_write(start_addr, buf, length) ! 0) { safe_fs_write_end(); return USBD_FAIL; } } else { /* 对齐的写入可以直接进行 */ if (SPI_FLASH_Write(buf, start_addr, length) ! 0) { safe_fs_write_end(); return USBD_FAIL; } } /* 更新写入缓存如果有的话 */ update_write_cache(start_addr, buf, length); } safe_fs_write_end(); return USBD_OK; }这里有几个优化点超时机制SD卡写入设置5秒超时防止卡死地址对齐检查SPI Flash通常需要按扇区擦除如果写入不对齐需要特殊处理写入缓存更新保持缓存一致性方便并发读取4.3 就绪与写保护状态这两个函数看似简单但很重要int8_t STORAGE_IsReady_FS(uint8_t lun) { if (lun 0) { /* 检查SD卡是否就绪 */ return (HAL_SD_GetCardState(hsd1) HAL_SD_CARD_TRANSFER) ? USBD_OK : USBD_FAIL; } else if (lun 1) { /* SPI Flash总是就绪除非正在擦除 */ return (spi_flash_busy() 0) ? USBD_OK : USBD_FAIL; } return USBD_FAIL; } int8_t STORAGE_IsWriteProtected_FS(uint8_t lun) { /* 可以根据需要动态控制写保护 */ if (lun 0) { /* SD卡通常有物理写保护开关这里可以读取GPIO状态 */ return (HAL_GPIO_ReadPin(SD_WP_GPIO_Port, SD_WP_Pin) GPIO_PIN_SET) ? USBD_OK : USBD_FAIL; } else if (lun 1) { /* SPI Flash的写保护由我们软件控制 */ return (flash_software_write_protect) ? USBD_OK : USBD_FAIL; } return USBD_FAIL; /* 默认不写保护 */ }STORAGE_IsWriteProtected_FS的返回值有点反直觉返回USBD_OK表示写保护启用即不能写入返回USBD_FAIL表示可以写入。这个设计来自USB MSC协议确实容易搞错。5. 实战调试技巧与性能优化配置都做好了代码也写完了但实际调试中还是会遇到各种问题。我整理了一些实用的调试技巧。5.1 常见问题排查问题1电脑不识别设备或识别为未知设备首先检查USB描述符。可以用USBlyzer或Wireshark抓包看看枚举过程。常见原因端点配置错误MSC需要Bulk-IN和Bulk-OUT端点描述符长度不对字符串描述符编码问题要用UNICODE问题2能识别但容量显示为0这通常是STORAGE_GetCapacity_FS函数返回了错误值。检查块数量计算是否正确块大小是不是严格512SD卡是否初始化成功可以在函数里加调试输出printf(LUN %d: blocks%lu, block_size%u\n, lun, *block_num, *block_size);问题3写入速度极慢可能的原因和解决方案可能原因检查方法解决方案SPI时钟太低测量SCK频率提高SPI时钟但不要超过Flash规格没有使用DMA查看CPU占用率启用SPI DMA传输块大小太小查看每次传输的块数主机端尝试格式化时指定更大的分配单元Flash擦除太频繁监控擦除操作实现写缓存合并多次写入问题4频繁出现磁盘需要格式化这是文件系统损坏的典型表现。可能的原因突然断电或拔出并发写入冲突FAT表或目录项不一致可以添加文件系统健康检查uint8_t check_fatfs_health(uint8_t lun) { FATFS fs; FRESULT res; char path[16]; sprintf(path, %d:, lun); /* 0: 或 1: */ res f_mount(fs, path, 0); /* 不立即挂载只检查 */ if (res ! FR_OK) { printf(LUN %d FS error: %d\n, lun, res); return 0; } /* 尝试打开根目录 */ DIR dir; res f_opendir(dir, path); if (res FR_OK) { f_closedir(dir); } return (res FR_OK); }5.2 性能优化实践优化1SPI Flash的写入缓存SPI Flash的写入速度瓶颈往往是擦除操作。一个扇区通常4KB擦除要几十毫秒而写入只要几毫秒。我们可以实现一个写缓存#define WRITE_CACHE_SIZE 4096 typedef struct { uint8_t data[WRITE_CACHE_SIZE]; uint32_t start_addr; uint32_t length; uint8_t dirty; } write_cache_t; write_cache_t spi_cache; void spi_cache_init(void) { memset(spi_cache, 0, sizeof(spi_cache)); spi_cache.dirty 0; } int spi_cache_write(uint32_t addr, uint8_t *buf, uint32_t len) { /* 如果缓存空或地址不连续或缓存满先刷写旧缓存 */ if (spi_cache.dirty (addr ! spi_cache.start_addr spi_cache.length || spi_cache.length len WRITE_CACHE_SIZE)) { spi_cache_flush(); } /* 初始化缓存如果是第一次或刚刷写过 */ if (!spi_cache.dirty) { spi_cache.start_addr addr; spi_cache.length 0; } /* 复制数据到缓存 */ memcpy(spi_cache.data spi_cache.length, buf, len); spi_cache.length len; spi_cache.dirty 1; /* 如果缓存满了立即刷写 */ if (spi_cache.length WRITE_CACHE_SIZE) { return spi_cache_flush(); } return 0; } int spi_cache_flush(void) { if (!spi_cache.dirty || spi_cache.length 0) { return 0; } /* 确保地址按扇区对齐 */ uint32_t aligned_addr spi_cache.start_addr ~(SPI_FLASH_SECTOR_SIZE - 1); uint32_t aligned_len ((spi_cache.length SPI_FLASH_SECTOR_SIZE - 1) ~(SPI_FLASH_SECTOR_SIZE - 1)); /* 需要先擦除整个扇区 */ SPI_FLASH_Erase_Sector(aligned_addr / SPI_FLASH_SECTOR_SIZE); /* 写入数据 */ int ret SPI_FLASH_Write(spi_cache.data, aligned_addr, aligned_len); spi_cache.dirty 0; spi_cache.length 0; return ret; }这个缓存机制可以将多次小写入合并为一次大写入减少擦除次数提升写入速度3-5倍。优化2SD卡的读写策略SD卡也有优化空间。默认的HAL库函数每次读写都要检查卡状态可以稍微优化/* 自定义的批量读取函数 */ int sd_read_multiple_blocks(uint32_t start_block, uint8_t *buf, uint32_t num_blocks) { HAL_StatusTypeDef status; /* 一次性检查状态而不是每次读写都检查 */ if (HAL_SD_GetCardState(hsd1) ! HAL_SD_CARD_TRANSFER) { return -1; } /* 根据块数选择最佳策略 */ if (num_blocks 1) { status HAL_SD_ReadBlocks(hsd1, buf, start_block, 1, 1000); } else if (num_blocks 64) { /* 中等数量块使用DMA */ status HAL_SD_ReadBlocks_DMA(hsd1, buf, start_block, num_blocks); if (status HAL_OK) { /* 等待DMA完成 */ while (HAL_SD_GetCardState(hsd1) HAL_SD_CARD_TRANSFER) { __NOP(); } } } else { /* 大量块分多次读取避免超时 */ uint32_t blocks_per_chunk 64; uint32_t chunks num_blocks / blocks_per_chunk; uint32_t remaining num_blocks % blocks_per_chunk; for (uint32_t i 0; i chunks; i) { status HAL_SD_ReadBlocks_DMA(hsd1, buf i * blocks_per_chunk * 512, start_block i * blocks_per_chunk, blocks_per_chunk); if (status ! HAL_OK) break; while (HAL_SD_GetCardState(hsd1) HAL_SD_CARD_TRANSFER) { __NOP(); } } /* 读取剩余块 */ if (status HAL_OK remaining 0) { status HAL_SD_ReadBlocks_DMA(hsd1, buf chunks * blocks_per_chunk * 512, start_block chunks * blocks_per_chunk, remaining); } } return (status HAL_OK) ? 0 : -1; }优化3USB传输的块大小调整虽然MSC协议要求块大小是512字节但我们可以一次传输多个块。在usbd_storage_if.c中/* 建议的传输配置 */ #define STORAGE_BLK_NBR 0x10000 /* 不重要实际容量由GetCapacity返回 */ #define STORAGE_BLK_SIZ 512 /* 固定512 */ /* 但在USB配置中可以调整MSC_MEDIA_PACKET */ #define MSC_MEDIA_PACKET 2048 /* 一次传输4个块 */较大的媒体包可以减少协议开销提升传输效率。但要注意不能超过USB端点的最大包大小需要足够的RAM做缓冲区有些老主机可能不支持大包5.3 电源管理与稳定性H743的双存储方案对电源要求比较高特别是同时操作SD卡和SPI Flash时。几个建议电源去耦每个芯片的VCC都要加100nF陶瓷电容SD卡槽附近加10μF钽电容信号完整性SDIO和SPI的时钟线要尽量短必要时串联22Ω电阻电流需求SD卡在写入时峰值电流可能超过100mA确保你的电源能提供足够电流可以在代码中加入电源监控void check_power_status(void) { /* 读取内部电压参考 */ uint32_t vref __HAL_ADC_CALC_VREFANALOG_VOLTAGE( HAL_ADCEx_Calibration_GetValue(hadc1, ADC_SINGLE_ENDED), ADC_RESOLUTION_12B); if (vref 3300) { /* 3.3V系统低于3.3V报警 */ printf(Warning: Voltage low: %lumV\n, vref); /* 降低时钟频率节省功耗 */ if (vref 3000) { reduce_sdio_clock(); reduce_spi_clock(); } } }调试这种双存储系统时逻辑分析仪几乎是必备的。我习惯同时抓取四路信号USB的D线SPI的SCK和CSSDIO的CLK一个GPIO作为调试标记这样当出现问题时可以清楚地看到是哪个环节的时序不对。最后分享一个我实际项目中遇到的坑SD卡和SPI Flash共用SPI总线。最初为了节省引脚我把SPI Flash和SD卡SPI模式接在同一个SPI外设上只是用不同的片选。结果发现当USB同时访问两个设备时经常出现SPI冲突。后来改用独立的SPI和SDIO接口问题就解决了。所以如果条件允许尽量给每个存储设备独立的接口硬件上的简化往往会在软件上带来更多复杂度。