嵌入式工业数据采集编程:实现1000Hz高频信号的实时存储与本地缓存
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
卡满轮转文件,旧文件清理





