当前位置:首页 > 工业控制 > 工业控制
[导读]1000Hz采集≠每毫秒写一次SD卡。Flash/SD的写入延迟是毫秒级,文件系统还伴随FAT更新,主循环里“采一帧写一帧”必卡死、必丢数据。 正确做法是三级解耦:ADC/DMA进RAM环形缓冲 → 主任务攒扇区 → 后台批量落盘,采集和存储完全异步。


1000Hz采集≠每毫秒写一次SD卡。Flash/SD的写入延迟是毫秒级,文件系统还伴随FAT更新,主循环里“采一帧写一帧”必卡死、必丢数据。 正确做法是三级解耦:ADC/DMA进RAM环形缓冲 → 主任务攒扇区 → 后台批量落盘,采集和存储完全异步。

一、架构分层

TIM触发ADC ─▶ DMA ─▶ SRAM环形缓冲

                             │

                       主循环/写盘任务

                             │

                      FIFO攒满4KB块

                             │

                   SDIO/SD卡 FATFS批量写

采集是硬实时,落盘是软实时。中断里只搬数据,不碰文件系统。

二、环形缓冲:中断写、任务读

单生产者(采样中断)单消费者(写盘任务),head/tail加volatile,不用加锁。

#define RB_SIZE 4096          // 2的幂

#define RB_MASK (RB_SIZE-1)


typedef struct {

   uint8_t buf[RB_SIZE];

   volatile uint16_t head;   // 写指针(中断)

   volatile uint16_t tail;   // 读指针(任务)

} ring_t;


ring_t rb;


void adc_isr_push(uint8_t v){

   uint16_t next = (rb.head+1)&RB_MASK;

   if(next==rb.tail) return;     // 满则丢新数据,别覆盖未写盘

   rb.buf[rb.head]=v;

   rb.head=next;

}


int rb_read(uint8_t*out){

   if(rb.tail==rb.head) return -1;

   *out=rb.buf[rb.tail];

   rb.tail=(rb.tail+1)&RB_MASK;

   return 0;

}

缓冲深度按“消费周期×采样率”估:1000Hz、写盘任务最坏200ms才来取,至少留200帧余量,实际给4KB~8KB稳一点。

多通道建议直接存样本结构体而不是单字节:

typedef struct{ uint32_t ts_ms; int16_t ch[4]; }sample_t;

ringbuf元素换成sample_t,DMA双缓冲更顺。

三、批量落盘:攒满扇区再写

SD卡单块写有效吞吐约2MB/s,多块连续写(CMD25)能到18~20MB/s,差近10倍。 FATFS里每调一次f_write都可能动FAT表,频繁小写既慢又伤卡寿命。

#define SEC 512

static uint8_t blk[SEC];

static uint16_t bi;


void feed_sample(sample_t*s){

   memcpy(blk+bi, s, sizeof(sample_t));

   bi += sizeof(sample_t);

   if(bi+sizeof(sample_t) > SEC){

       f_write(&fil, blk, SEC, &bw);

       bi=0;

   }

}


void flush_periodic(void){

   if(bi>0){

       f_write(&fil, blk, bi, &bw);

       f_sync(&fil);          // 关键数据周期sync,别每帧sync

       bi=0;

   }

}

要点:

块大小按SD卡扇区512或4KB页对齐,别从奇数偏移写

文件打开一次持续追加,别每帧f_open/f_close

f_sync频率折中:纯日志5~10s一次,关键数据每次flush

写盘任务优先级低于采集中断,但高于空闲任务

缓冲快满时提高落盘频率或降采样,别硬覆盖

四、DMA双缓冲更稳

单字节中断在1000Hz还行,多通道+24位ADC建议DMA循环模式:

/* HAL例:ADC DMA半传输/全传输回调 */

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef*h){

   adc_ring_write(dma_buf, 0, HALF);

}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef*h){

   adc_ring_write(dma_buf, HALF, HALF);

}

CPU只处理“另一半”数据,采集不卡。

五、掉电保护:别信“写完了”

SD卡写一半拔电,FAT表坏掉整卡只读是常态。

硬件层:

电源入口加储能电容/超级电容,掉电中断触发后留几ms完成当前块写入

SD卡单独LDO供电,别从MCU 3.3V拉电

驱动层:

先写数据块,再更新文件长度/索引

文件头放“关闭标志”,启动时检查,不完整就标记损坏隔离

应用层:

关键帧带seq+CRC,回读校验

参数/校准存EEPROM或NOR,别和日志混在SD卡

环形日志限额写,不无限追加

void power_fail_irq(void){

   flush_partial();        // 把blk里残数据写完

   f_sync(&fil);

   backup_volatile_meta(); // 写最后偏移量到EEPROM

}

六、落坑清单

每样本f_write→采集卡死

中断里f_open/f_close→文件系统状态炸

ringbuf不满判断错→空满混淆丢数据

head/tail不加volatile→编译器缓存旧值

中断里做解析/printf→ISR拖太长丢采样

非512对齐写SD→读-改-写掉速

只写不f_sync→掉电全丢

SD卡从MCU 3.3V取电→写瞬间复位

缓冲满了覆盖旧数据→丢未存样本

日志无限追加→卡满业务崩

七、落地顺序

TIM定1ms触发ADC,DMA搬采样值

SRAM建ringbuf,中断只push

主循环/RTOS任务读ringbuf攒块

攒满512B或定时flush,f_write批量写

文件一次打开持续追加,周期f_sync

掉电检测中断+储能电容兜底

关键meta(偏移/seq)存EEPROM

卡满轮转文件,旧文件清理



本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭