STM32F103(Bluepill)SPI SD卡中断写入失败及主循环异常求助
问题原因分析
1. 中断服务函数内无法写入的核心原因
- FatFs的核心API(
f_mount、f_open、f_close等)不支持在中断上下文调用,这类函数会修改全局文件系统状态,中断里执行会和主程序的操作产生竞态条件,直接破坏文件系统一致性,导致写入失败。 - 中断服务函数要求执行周期极短(远小于10ms),但SD卡的SPI读写、文件系统操作是阻塞式的,会大幅拉长中断响应时间,导致系统其他功能异常,甚至触发硬件看门狗复位。
2. main循环写入异常的原因
- 数据采集和写入的时序不匹配:中断每10ms产生一组数据,但main循环如果没有及时读取缓存的采集数据,新数据会覆盖旧数据,导致丢失;同时如果写入操作耗时超过10ms,会错过多组数据,最终记录的数据量不足。
- 每次写入都重复执行
f_mount、f_open、f_close:这些操作开销极大(尤其是SD卡初始化和文件打开/关闭),频繁执行会导致写入速度跟不上采集速度,同时反复打开关闭文件可能导致文件系统缓存未及时刷新,出现重复写入的脏数据。
解决方案
步骤1:采用「中断采集+主循环写入」的异步架构
- 中断服务函数只做最轻量化的操作:采集4个28bit数值,拼接成字符串后存入环形缓冲区,立即退出中断。
- 主循环专注于从环形缓冲区读取数据,批量写入SD卡,避免频繁的文件操作。
步骤2:优化文件操作逻辑
- 仅在系统初始化时执行一次
f_mount,不要每次写入都挂载。 - 系统启动时打开目标文件(
f_open),保持文件句柄一直打开,直到系统停止或需要切换文件时再执行f_close。 - 每次写入前无需调用
f_lseek,文件以FA_WRITE打开后,写入指针会自动指向文件末尾;若需确认位置,可偶尔用f_size校验,但无需每次执行。
步骤3:实现线程安全的环形缓冲区
- 用原子操作或关闭中断的方式保护读写指针,避免中断和主程序同时操作缓冲区导致数据错乱。
- 缓冲区大小至少能存储100组数据(对应1秒采集量),避免主循环写入延迟时数据溢出。
优化后的代码示例
1. 全局变量定义
#define BUFFER_SIZE 128 // 可根据实际调整环形缓冲区大小 FIL fil; FATFS fs; char data_buffer[BUFFER_SIZE][64]; // 假设每组数据字符串最长64字节 uint8_t buf_write_ptr = 0; uint8_t buf_read_ptr = 0; volatile uint8_t buf_has_data = 0; // 标记缓冲区是否有未处理数据
2. 中断服务函数
void EXTIx_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Linex) != RESET) { // 采集4个28bit数值 uint32_t val1 = get_sensor_data(1); uint32_t val2 = get_sensor_data(2); uint32_t val3 = get_sensor_data(3); uint32_t val4 = get_sensor_data(4); // 拼接成字符串 snprintf(data_buffer[buf_write_ptr], 64, "%lu,%lu,%lu,%lu\r\n", val1, val2, val3, val4); // 原子操作更新环形缓冲区写指针 __disable_irq(); buf_write_ptr = (buf_write_ptr + 1) % BUFFER_SIZE; buf_has_data = 1; __enable_irq(); EXTI_ClearITPendingBit(EXTI_Linex); } }
3. main循环中的写入逻辑
int main(void) { // 系统初始化:GPIO、SPI、EXTI、UART等 SystemInit(); HAL_Init(); init_sensors(); init_spi_sdcard(); init_exti_interrupt(); // 初始化FatFs FRESULT res = f_mount(&fs, "", 0); if(res != FR_OK) { // 挂载失败,处理错误(如点亮错误LED) while(1); } // 打开文件并保持句柄打开 res = f_open(&fil, "data.txt", FA_OPEN_ALWAYS | FA_WRITE); if(res != FR_OK) { // 打开失败,处理错误 while(1); } // 将写入指针移到文件末尾 f_lseek(&fil, fil.fsize); while(1) { if(buf_has_data) { __disable_irq(); // 读取缓冲区数据 char* write_data = data_buffer[buf_read_ptr]; buf_read_ptr = (buf_read_ptr + 1) % BUFFER_SIZE; // 检查缓冲区是否为空 if(buf_read_ptr == buf_write_ptr) { buf_has_data = 0; } __enable_irq(); // 写入SD卡 UINT bw; res = f_write(&fil, write_data, strlen(write_data), &bw); if(res != FR_OK) { // 写入失败,处理错误 } // 定期刷新缓存(每写入10组数据刷新一次,避免掉电丢失) static uint8_t write_count = 0; if(++write_count >= 10) { f_sync(&fil); write_count = 0; } } // 主循环可处理其他低优先级任务 // ... } // 程序结束时关闭文件(实际项目可通过按键触发) f_close(&fil); f_mount(NULL, "", 0); }
额外注意事项
- 合理设置SPI总线时钟:STM32F103的SPI最高可达18MHz,但SD卡兼容性可能要求初始化为低频率(如400kHz),初始化完成后再切换到高频率。
- 确保SD卡写保护关闭,硬件连接正确(尤其是CS引脚的电平控制)。
- 若系统有看门狗,需保证主循环写入操作不会导致看门狗超时,必要时在循环中喂狗。
内容的提问来源于stack exchange,提问作者Mr. Black
相关产品推荐
相关产品推荐

