news 2026/10/19 4:38:20

STM32定时器触发ADC DMA采样:原理、配置与避坑实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32定时器触发ADC DMA采样:原理、配置与避坑实践

简介:面向STM32F1嵌入式开发者,这份压缩包聚焦TIMER3的TRGO事件触发ADC采样,并通过DMA自动将转换结果存入缓存,适用于需要高效、低CPU占用实现定时采集的工程场景。包内共6个文件,包含3个C源文件与3个H头文件,分别实现ADC、TIMER3、DMA的初始化和中断服务,整体仅3KB,结构精简,便于直接移植学习。已有4106人学习下载,配置思路和代码模块得到了较多关注。资源中给出了如init_ADC、init_TIMER3、init_DMA等初始化函数,以及DMA传输完成中断处理与缓存切换的参考写法,可帮助读者快速掌握外设联动配置,理解TRGO触发、ADC转换结束与DMA搬运之间的时序关系,节省底层调试时间。对于希望提升系统实时性、减少CPU干预的STM32开发者,是一份实用的参考代码。

1. 定时器触发 ADC DMA 这条链路,到底解决什么问题

电机控制、电网谐波分析、音频采样这类场景里,CPU 给 ADC 发“开始转换”命令是最容易出错的做法——中断响应延迟在几百纳秒到几微秒之间漂移,采样间隔只要抖动一次,后面做 FFT 算出来的相位、谐波分量全是错的。STM32 的 TIMER3 TRGO 触发 ADC、再由 DMA 自动把结果搬进内存,核心价值就是让采样时序脱离软件控制:定时器溢出那一刻硬件发出触发信号,ADC 在下一个时钟周期启动转换,转换完成硬件拉起 DMA 请求,数据落内存后 CPU 完全不知情。整个过程不需要进中断、不需要软件干预,采样周期由定时器的 PSC 和 ARR 两个寄存器唯一决定。这份资源适合刚把标准库点亮的开发者,也适合被采样抖动折腾过的老手——本篇就把寄存器配置、周期计算和最容易翻车的几个边界一次讲透。

2. TIM3 TRGO 信号链配置:先让定时器“广播”触发事件

2.1 TRGO 不是随便一根输出线:主模式选择位 MMS 决定事件源

STM32 定时器的 TRGO(Trigger Output,触发输出)是内部信号,不经过 GPIO,它的用途是把定时器内部事件“广播”给其他外设。ADC、DAC、另一个定时器都可以监听这根线。很多人第一次写代码时只配置了定时器的 PSC 和 ARR,结果 ADC 纹丝不动,因为根本没告诉定时器“你要把哪个事件送出去”。

TRGO 的事源选择在定时器的 CR2 寄存器里,对应控制位叫 MMS(Master Mode Selection)。标准库封装成了TIM_SelectOutputTrigger()函数,它有四个常用选项:

// 把 TIM3 的更新事件作为 TRGO 输出 TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); // 把 TIM3 的使能信号作为 TRGO 输出 // TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Enable); // 把 TIM3 的 OC1REF 信号作为 TRGO 输出 // TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_OC1Ref);

绝大多数 ADC 定时触发场景用更新事件(Update Event)。所谓更新事件,就是计数器向上计数溢出、准备回零重载那一刻产生的脉冲。TIM_TRGOSource_Enable 是另一个容易误用的选项——它输出的是定时器使能电平,不是脉冲,如果选它,ADC 只会在定时器刚打开时触一次,之后再也不触发。OC1Ref 选项适合要在特定比较点触发 ADC 的场景,比如 PWM 中心对齐采样,但这个资源里不需要,保持默认的更新事件就行。

配置完 MMS 之后,还要注意一个细节:定时器必须处于使能状态才会产生更新事件,也就是TIM_Cmd(TIM3, ENABLE)不能漏。这句漏了,ADC 还是完全不触发。很多入坑者在这里卡半天,查了 ADC、DMA 寄存器都没问题,最后发现定时器没开。

2.2 采样周期计算:PSC 和 ARR 不是随便填的

定时器触发 ADC 的采样周期由公式T_sample = (PSC + 1) × (ARR + 1) / TIM_CLK决定。以一个主频 72 MHz 的芯片为例,TIM3 挂载在 APB1 总线上,通常 TIM_CLK 也是 72 MHz(APB1 分频不为 1 时定时器时钟会翻倍)。如果想得到 10 kHz 的采样率(即 100 μs 触发一次 ADC),有无数种 PSC/ARR 组合:

采样率PSCARR实际采样周期频率误差
10 kHz07199100.00 μs0%
10 kHz7899100.00 μs0%
10 kHz7199100.00 μs0%
50 kHz0143920.00 μs0%
50 kHz717920.00 μs0%
1 kHz719991000.00 μs0%

那到底怎么选 PSC 和 ARR?我的习惯是:先把 ARR 压到足够小,让计数器的分辨率足够高,但不要小到更新事件频率超过 ADC 的转换能力。ADC 完成一次转换需要的时间是采样周期加转换周期,比如采样周期 1.5 个 ADC 时钟、转换 12.5 个 ADC 时钟,ADC 时钟 12 MHz 的话,单次转换大约 1.17 μs。如果定时器 1 μs 就触一次,ADC 根本转不完,DMA 搬运就会出现丢数据。所以确定采样率之前,先把 ADC 的时钟和采样周期算清楚,确保T_sample > 单次转换时间,这是硬件链路能稳定工作的前提。

代码里通常用宏定义把周期参数跟实际采样率对应起来:

#define TIM3_PSC 71 // 72 MHz / (71+1) = 1 MHz 计数频率 #define TIM3_ARR 99 // 1 MHz / (99+1) = 10 kHz 触发频率 void TIM3_Init(void) { TIM_TimeBaseInitTypeDef tim_base; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); tim_base.TIM_Prescaler = TIM3_PSC; tim_base.TIM_CounterMode = TIM_CounterMode_Up; tim_base.TIM_Period = TIM3_ARR; tim_base.TIM_ClockDivision = TIM_CKD_DIV1; tim_base.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM3, &tim_base); TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); TIM_Cmd(TIM3, ENABLE); }

这段代码的关键逻辑是先把 PSC 设为 71,把计数频率从 72 MHz 降到 1 MHz,再去计 ARR=99,也就是每 100 个计数脉冲产生一次更新事件。为什么要先分频再计数?因为如果 PSC=0、ARR=7199,计数器每一步是 13.9 ns,ARR 寄存器里写 7199 也完全可行,没有本质区别。但当你需要临时改采样率做实验时,固定 PSC=71、只改 ARR,改动更直观,也更不容易算错。TIM_RepetitionCounter 只有高级定时器才用,TIM3 是通用定时器,标准库里写上也不会生效,但保留它是向上兼容的习惯。

2.3 开启更新中断?不,这个场景不需要

新手容易惯性思维:定时器产生事件,就要开中断。但在 TRGO 触发 ADC 的场景里,默认不开定时器更新中断,因为中断会抢占 ADC 的 DMA 传输,反而引入时序抖动。那如果确实想知道定时器触发频率是否正常怎么办?资源里给的做法是用一个 IO 翻转来观测,不是进中断:

// 用于观测触发频率的调试方法:PA8 翻转 void TIM3_Update_Event_Probe(void) { static uint8_t toggle = 0; // 读取 SR 寄存器中的 UIF 标志 if (TIM_GetFlagStatus(TIM3, TIM_FLAG_Update) == SET) { // 手动清除标志,不开启中断,仅供调试时查询 TIM_ClearFlag(TIM3, TIM_FLAG_Update); toggle = !toggle; GPIO_WriteBit(GPIOA, GPIO_Pin_8, toggle); } }

这段代码放在主循环里轮询,配合示波器看 PA8 的波形周期,就能确认定时器更新事件的实际频率。查询 UIF 标志后要立即手动清除,因为 TRGO 输出的是脉冲信号,但 UIF 标志是电平锁存的,不清除的话下一次轮询仍然读到 SET,测出来的频率就是错的。

3. ADC 触发源与扫描模式:外部触发边沿和采样周期决定数据质量

3.1 把 ADC1 的触发源指向 TIM3_TRGO

定时器那边把事件广播出来了,ADC 这边还得“同意收听”。ADC 的控制寄存器 CR2 里有两个位:EXTTRIG 和 EXTSEL。EXTTRIG 是外部触发使能位,EXTSEL 选择具体是哪个外设的触发信号。每个芯片型号的 EXTSEL 映射表略有不同,以常见的 F1 系列为例:

EXTSEL 值规则组触发源适用定时器
000TIM1_CC1TIM1 比较通道 1
001TIM1_CC2TIM1 比较通道 2
010TIM1_CC3TIM1 比较通道 3
011TIM2_CC2TIM2 比较通道 2
100TIM3_TRGOTIM3 触发输出
101TIM4_CC4TIM4 比较通道 4

也就是说,TIM3_TRGO 对应的是 EXTSEL=100,不是默认值。标准库初始化代码里要显式指定:

ADC_InitTypeDef adc_init; // 选择 TIM3_TRGO 作为规则组触发源 adc_init.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO; // 上升沿触发 adc_init.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising; // 关闭连续转换,只靠外部触发逐个转 adc_init.ADC_ContinuousConvMode = DISABLE; // 多通道扫描模式 adc_init.ADC_ScanConvMode = ENABLE;

这几个参数有个先后逻辑:关闭连续转换是必须的,因为一旦开了连续转换,ADC 会自己不停地转,外部触发信号根本插不进来;扫描模式打开是让 ADC 在收到一次触发后把规则序列里的所有通道依次转完。如果只采一个通道,扫描模式开不开都行,但多通道下忘记打开 ScanConvMode,ADC 就只采第一个通道。ADC_ExternalTrigConvEdge 选上升沿还是下降沿,跟 TRGO 的脉冲极性有关。更新事件作为 TRGO 输出时,默认是一个正脉冲,所以用上升沿触发。

初始化顺序上还有一个隐藏依赖:先配 ADC,再使能 ADC,最后等 ADC 稳定,然后打开定时器。顺序反了会漏掉第一轮采样,导致 DMA 缓冲区开头出现几个 0 值。

3.2 采样周期不是越大越好:从 AC 采样精度说起

ADC 的采样周期(SAMPLING TIME)决定采样保持电容充电的时间。外部信号源阻抗越高,需要的采样时间越长。对于普通的电压采样,1.5 个周期通常够用;但如果信号源是分压电阻网络,电阻值超过 10 kΩ,1.5 个周期的采样值会偏低,转换结果产生幅值误差。这个误差不像非线性和偏移那样好校准——它会随输入电压变化而改变,在交流采样里表现为波形削顶或幅值压缩。

// ADC 1.5 cycle 采样周期 vs 239.5 cycle 采样周期 // 12 MHz ADC 时钟下: // 1.5 cycle -> 125 ns 采样时间 // 239.5 cycle-> 19.96 us 采样时间 void ADC_Channel_SampleTime_Config(void) { // 采样时间按通道单独配置 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_1Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_1Cycles5); // 高阻信号源通道,加大采样时间 ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_239Cycles5); }

这段配置里,通道 0 和通道 1 都是低阻信号源,1.5 周期采样足够;通道 2 接的是高阻分压网络,选择 239.5 周期。要注意的是,加大采样时间会拉长单次转换的总耗时,进而影响最大可支持的触发频率。算一下:ADC 时钟 12 MHz,采样 239.5 周期加转换 12.5 周期,单通道总耗时 21 μs。如果定时器触发频率是 50 kHz(20 μs 周期),ADC 上一次还没转完,下一次触发就来了,DMA 会搬走上一轮的数据,导致采样序列错位。遇到这种情况,要么降低采样率,要么换低阻抗信号源,没有别的捷径。

3.3 校准序列:规则组通道顺序和采样率的配合

多通道采集时,规则组的序列寄存器(SQR)决定了一次触发后按什么顺序转换哪些通道。序列长度和通道顺序直接影响每次触发的时间开销。比如配置了 3 个通道,一次触发后要转 3 个通道,总时间就是 3 个通道的采样时间加转换时间之和,这决定了最大触发频率。

// 规则组序列配置:3 个通道,顺序 0-1-2 void ADC_RegularSequence_Config(void) { // SQR1 寄存器,设置规则组序列长度为 3 ADC_InitTypeDef adc_init; adc_init.ADC_NbrOfChannel = 3; // 规则组序列长度 3 adc_init.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO; adc_init.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising; adc_init.ADC_ContinuousConvMode = DISABLE; adc_init.ADC_ScanConvMode = ENABLE; adc_init.ADC_DataAlign = ADC_DataAlign_Right; ADC_Init(ADC1, &adc_init); // 序列 1: 通道 0 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_1Cycles5); // 序列 2: 通道 1 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_1Cycles5); // 序列 3: 通道 2 ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_1Cycles5); }

如果触发频率高于单轮序列转换总耗时,规则组会出现丢转换的情况:ADC 会在忙的时候把新的触发请求挂起,但不会排队等,直接丢弃。等定时器频率降下来,数据又会恢复正常。调试时看到波形周期性少点,优先怀疑这个原因,不要先怀疑 DMA。

4. DMA 循环存取:用半传输中断做无缝双缓冲

4.1 DMA 通道映射别想当然

ADC 的 DMA 请求不是随便挂到哪个 DMA 通道都能响应的。标准库和参考手册里都有一张映射表,ADC1 的 DMA 请求在 F1 系列上固定走 DMA1 的通道 1,ADC3 则走 DMA2 的通道 0(如果有 DMA2 的话)。把 DMA 配置到其他通道,初始化时不报错,但 DMA 永远不会收到请求,这是典型的“代码能跑但不干活”的问题。

#define ADC_BUF_LEN 1024 uint16_t adc_buf[ADC_BUF_LEN]; // 1024 次采样结果的存放区 volatile uint8_t half_complete = 0; // 半传输标志 volatile uint8_t full_complete = 0; // 全传输标志 void DMA_ADC1_Init(void) { DMA_InitTypeDef dma_init; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); dma_init.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 外设地址:ADC 数据寄存器 dma_init.DMA_MemoryBaseAddr = (uint32_t)adc_buf; // 内存地址:用户缓冲区 dma_init.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设是源,读取 ADC 数据 dma_init.DMA_BufferSize = ADC_BUF_LEN; // 传输次数要素 dma_init.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增 dma_init.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增 dma_init.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 16 位 dma_init.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 16 位 dma_init.DMA_Mode = DMA_Mode_Circular; // 循环模式 dma_init.DMA_Priority = DMA_Priority_High; dma_init.DMA_M2M = DMA_M2M_Disable; // 不是内存到内存 DMA_Init(DMA1_Channel1, &dma_init); DMA_ITConfig(DMA1_Channel1, DMA_IT_TC | DMA_IT_HT, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }

外设地址写&ADC1->DR而不是某个数组名,是因为 DMA 需要的是地址值。DMA_PeripheralDataSize_HalfWord对应 ADC 的 12 位数据右对齐后刚好是 16 位,这里用 HalfWord 两边都对齐,避免数据宽度不一致导致的高低位错乱。DMA_Mode_Circular 是循环模式,跑完一轮自动从首地址重新开始,整个缓冲区的写入不会中断。M2M 必须禁用,因为这是外设到内存的传输,不是内存到内存拷贝;M2M 模式下 DMA 会忽略触发请求,让 DMA 误以为是一次性内存拷贝,ADC 的数据就不会再进来。

4.2 用半传输中断替代双缓冲:读写分离的关键

DMA 循环模式配好了,但 CPU 读数据时不能直接去数组里拿——如果你刚好读到一半,下一次 DMA 写入已经把另外一半覆盖了,数据就是旧一半混新一半。常见做法是用双缓冲,但 STM32 的 DMA 还可以用半传输中断 + 循环模式实现更简洁的双缓冲效果。当 DMA 传输完一半缓冲区(512 个 16 位数据)时触发 HT 中断,CPU 去处理前一半;传输完整个缓冲区时触发 TC 中断,CPU 去处理后一半。因为 DMA 始终在循环,CPU 处理前半段时 DMA 正在写后半段,处理后半段时 DMA 又从头开始写前半段,两边互不干扰。

void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_HT1)) // 半传输中断 { DMA_ClearITPendingBit(DMA1_IT_HT1); half_complete = 1; // 置标志,主循环里处理 } if (DMA_GetITStatus(DMA1_IT_TC1)) // 全传输中断 { DMA_ClearITPendingBit(DMA1_IT_TC1); full_complete = 1; } }

中断服务函数里不直接做数据处理,只置标志位,数据处理放在主循环里。为什么?因为 DMA 中断里做 FFT、串口打印这类耗时操作,会延长中断占用时间,下一次 DMA 中断就进不来了,严重时直接进 HardFault。主循环里通过标志位轮流访问缓冲区的前后两半:

while (1) { if (half_complete) { half_complete = 0; // 处理 adc_buf[0] ~ adc_buf[511] Process_ADC_Half(adc_buf, 512); } if (full_complete) { full_complete = 0; // 处理 adc_buf[512] ~ adc_buf[1023] Process_ADC_Half(&adc_buf[512], 512); } }

这里有一个隐含的边界问题:全传输中断标志置位后,DMA 不会立刻从头开始写,而是继续写完后才重新回到首地址。所以full_complete = 1的时刻,缓冲区后半段的数据是完整的,前半段正要被覆盖。主循环处理后半段时,DMA 正在写前半段——这个时序刚好错开。如果主循环太慢,处理 512 个点的时间超过 DMA 写满另一半的时间(取决于采样率),数据还是会被覆盖。解决办法是缩小半区大小,或者用更大的 DMA 缓冲区。这个权衡没有绝对标准,只能按采样率和主循环耗时来调节。

4.3 ADC 使能 DMA 请求:需要单独的一行

ADC 的 DMA 请求不是配好 DMA 通道就能自动使能的,还要在 ADC 的 CR2 寄存器里打开 ADC DMA 使能位。标准库对应的函数是ADC_DMACmd(ADC1, ENABLE)。漏掉这一句,DMA 配置写得再完整也收不到数据。

void ADC1_Init_For_DMA(void) { // ... 前述 ADC 初始化代码 ... ADC_DMACmd(ADC1, ENABLE); // 开启 ADC 的 DMA 请求输出 ADC_Cmd(ADC1, ENABLE); // 使能 ADC // 校准:等待校准完成,确保 ADC 内部电容状态正确 ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); }

使能 ADC 之后不要立刻启动定时器;先做校准。校准期间如果定时器触发已经开始,ADC 会忙于校准而忽略外部触发,第一轮采样数据全部丢失。这个顺序问题在资源给出的源码里是单独成章节的,因为它太容易被忽略了。校准结束后再TIM_Cmd(TIM3, ENABLE),这样第一个触发脉冲进来时,ADC 已经处于完全就绪状态。

5. 避坑排查:定时器触发 ADC DMA 的五个典型翻车现场

5.1 现象一:ADC 完全不转换,DMA 缓冲区全是 0

按顺序排查:先查定时器有没有产生更新事件,用示波器量 PA8 翻转波形;再查 MMS 是否设置为 Update 事件;然后查 ADC 的 EXTSEL,确认ADC_ExternalTrigConv真的是ADC_ExternalTrigConv_T3_TRGO;最后查 ADC 的 DMA 请求有没有使能。这个链路任何一环断了都是静默失败,因为寄存器全是可读的,但功能就是不走。

原因里最常见的是 EXTSEL 用默认值,默认值对应 TIM1_CC1,ADC 在等一个永远不来的信号。解决:显式将ADC_ExternalTrigConvEdge设置为上升沿,并将ADC_ExternalTrigConv设置为T3_TRGO,一个都不能省。我见过有人在标准库里把 edge 参数漏掉,结果 ADC 配置函数直接用了库默认值,触发信号来了也当没看见。

5.2 现象二:DMA 数据每隔固定长度错位,出现周期性的跳变点

数据不是每次都错,而是每 N 个点错一次,且 N 与 ADC 通道数或规则组序列长度有关。根因基本是:单次触发的转换总时间超过了定时器触发周期。比如序列长度 3、每个通道 1.5 周期采样,但定时器周期只比序列总时间短那么几百纳秒,偶尔一次触发落在 ADC 忙着的窗口里,被丢弃,后续所有序列都往后挪一个位置。

解决:一是加长定时器触发周期,优先把 ARR 往上调;二是缩短 ADC 采样周期,把 1.5 周期改成 1.5 周期以外更大的值时要重新核算;三是减少规则组通道数量。排查办法是在数据起始位置放一个已知的基准电压,比如通道 0 接 1.5 V 参考,检查数据流里基准值出现的间隔是否与规则组长度一致。

5.3 现象三:上半区数据正常,下半区数据全 0

这是半传输中断 + 循环模式的经典坑。全传输中断触发后,CPU 去处理后半段,但 DMA 可能在中断还没执行完时已经重新从首地址开始写。主循环处理后半段的速度赶不上 DMA 写前半段的速度,等 CPU 去读后半段,里面已经被新数据部分覆盖。

解决:把 DMA 缓冲区加大一倍,或者把半区的点数量减少。另一个思路是关闭循环模式,改用普通模式,每次 ADC 数据满缓冲区就产生 TC 中断,在中断里重新启动 DMA,缓冲区地址不变。但这样会损失循环模式的零开销优势,更推荐的做法是:调高 DMA 中断优先级,数据处理主循环里不要做阻塞操作,只有 Copy 操作,计算、打印全部放到另一层。

5.4 现象四:ADC 数据整体偏小或波形削顶

信号源阻抗过高、采样时间不够时,采样电容还没充满就被切断了,12 位 ADC 读到的值比实际输入低。这一条容易被误判成分压电阻算错了。解决:提高该通道的采样时间,把ADC_SampleTime_1Cycles5改为ADC_SampleTime_71Cycles5或ADC_SampleTime_239Cycles5。改完采样时间后要重新核算最大触发频率,防止转化时间超限。

5.5 现象五:HAL 库环境下启动顺序不对,数据总是丢前几帧

标准库的初始化时序相对可控,但使用 HAL 库时HAL_ADC_Start_DMA()和HAL_TIM_Base_Start()的先后顺序不同,行为差异明显。先启动定时器再启动 ADC DMA,会漏掉早期触发;先启动 ADC 再启动定时器,又可能因为 ADC 未校准而丢数据。解决:强制按“ADC 校准完成 → ADC DMA 启动 → 定时器启动”的顺序执行。这个顺序在 HAL 里没有文档明确写,但调过的都知道它是血泪经验。

6. 快速验证这套链路的两个技巧

先说一个最直接的验证技巧:把定时器的更新事件同时配置为输出比较翻转,用逻辑分析仪直接量触发频率。具体做法是在 TIM3 的更新事件产生时同步翻转一个 GPIO,这不需要额外中断,只要把同一个更新事件映射到 TRGO 的同时,再配置一个输出比较通道,让它也在更新事件时翻转。代码里就多几行:

TIM_OCInitTypeDef tim_oc; tim_oc.TIM_OutputState = TIM_OutputState_Enable; tim_oc.TIM_Pulse = 50; // 占空比设定,无关紧要 tim_oc.TIM_OCMode = TIM_OCMode_Toggle; TIM_OC1Init(TIM3, &tim_oc);

然后示波器探头夹在 GPIO 上,数脉冲波形,就能验证实际触发频率和理论值差多少。这个方法比进中断打印时间戳要干净得多,不会影响采样时序。

第二个技巧是验证 DMA 数据链路的完整性:在 ADC 输入引脚接一个已知频率的方波或正弦波,比如用另一个定时器输出 PWM 通过低通滤波后灌进 ADC 输入,采集完在串口或屏幕上观察波形。如果波形上的采样点数等于定时器频率除以信号频率,链路就是通的;如果点数偏少或波形出现重影,排查方向就锁定在 ADC 规则组序列长度和 DMA 缓冲区的边界对齐上。我一般会用 1 kHz 正弦波和 10 kHz 采样率组合,一个周期 10 个点,看起来特别清晰。

从那以后我每次做定时器触发 ADC 的工程,都强制先过一遍这个验证流程:先量定时器波形,再灌已知信号看采集结果,两步都通过才继续往下做算法。这套流程救过我好几次——有一次就是示波器波形完全正常,但采集到的正弦波周期点数偏了一个,后来查出来是规则组序列长度配置多了 1 个通道,导致每轮数据里混进了一个不存在的通道值。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 4:37:54

open-code-review:CLI驱动的代码审查新范式

1. “open-code-review”不是新工具,而是代码审查范式的转向信号最近在几个技术团队的内部分享会上,我反复听到“open-code-review”这个词被提起——不是作为某个具体开源项目的名字,也不是某家公司的私有产品代号,而是一种正在快…

作者头像 李华
网站建设 2026/10/19 4:37:30

Windows 11原生安装OpenClaw:本地模型配置与工具调用指南

OpenClaw在Windows 11上装起来,理论上一套文档跟着走很快,但我实际磕磕碰碰花了好几天。倒不是软件本身有多难,而是绝大多数说明默认你在Linux或macOS上跑,默认你能顺利拉取任何国际资源,默认你手头有现成的云端API密钥…

作者头像 李华
网站建设 2026/10/19 4:37:10

Cesium加载MVT矢量瓦片实战:从PBF解码到渲染优化

简介:围绕Cesium加载MVT矢量瓦片的开发资源包,面向需要在Web端实现三维地图与矢量瓦片渲染的前端/GIS开发者。内容梳理了MVT(Mapbox Vector Tile)与PBF编码格式的解析思路,并给出将瓦片几何数据转为Cesium可渲染对象的…

作者头像 李华
网站建设 2026/10/19 4:36:50

AI教材生成工具实战:低查重原创内容与人工校准流程

上个月某天晚上,有个做培训的朋友给我发消息:又要交教材稿了,几十页,下周一要,配了一个崩溃的表情。这位朋友在某家培训机构做课程研发,平时上课没问题,一到写教材就焦虑。其实不只是他&#xf…

作者头像 李华
网站建设 2026/10/19 4:36:09

GBase 8c故障定位:从操作系统层入手排查CPU、内存、磁盘与网络

1. 故障定位先别急着翻日志,先圈定现象边界做GBase 8c数据库运维这些年,我遇到最多的故障,根子都出在操作系统层。数据库进程跑在操作系统上,CPU怎么分配、内存够不够、磁盘写得动写不动、网络稳不稳,全都受系统层控制…

作者头像 李华
网站建设 2026/10/19 4:34:25

开源代码审查工具 open-code-review:规则引擎与质量看板实践

1. 为什么还要做一个开源的代码审查工具1.1 代码审查现状:不做不行,做起来又痛代码审查这件事,我相信但凡在正规团队待过两年以上的开发者,都有切身体会。它非常重要,能提前拦截 60% 以上的逻辑缺陷和设计问题&#xf…

作者头像 李华