简介本资源面向STM8单片机初学者与嵌入式硬件开发者聚焦STM8S003如何通过定时器触发ADC采样这一典型应用解决周期性模拟信号采集的配置难题。压缩包共41个文件约76KB以c与h源码为核心包含main、adc、tim1、delay、uart、led等模块另有xcl、bat、wsdt、dnx、dbgdt等IAR工程与调试配置文件以及hex、map、out等编译产物便于直接打开工程验证。已有3712人学习下载。读者可从中获得定时器时基与自动重载配置、ADC触发源选择、中断服务中启动转换并读取结果的完整实现思路理解采样速率与定时周期的对应关系并借助工程文件快速复现环境参数监测、电机控制、电源管理等场景下的周期采样方案。1. 定时器触发ADC采样为什么轮询方式迟早会让你翻车很多刚接触STM8的朋友做ADC采样的第一反应就是在主循环里调用库函数读一次结果处理一下再延时等待下一次。这种写法在单一任务、采样率要求不高的场景下确实能跑通但只要系统里多一个串口收发、多一个按键扫描采样间隔立刻变得飘忽不定。你以为是1kHz均匀采样实际可能是800Hz到1.2kHz之间随机跳动后面做FFT或者做闭环控制时数据根本没法用。STM8的ADC模块本身支持外部事件触发其中定时器TRGO触发是最常用的一种。配置好之后ADC硬件会在每个定时器溢出事件到来时自动启动一次转换CPU完全不需要干预采样时刻。这样做的好处有三个采样间隔由定时器硬件保证抖动在纳秒级CPU只需要在转换完成中断里取数据主循环该干嘛干嘛采样率可以通过修改定时器周期精确计算不用靠软件延时去凑。这套方案适合谁做电机控制需要电流采样的、做音频信号采集需要固定采样率的、做传感器阵列需要多通道轮询的都值得把轮询方式换成定时器触发。下面从寄存器配置到中断处理一步步拆开讲。2. 定时器TRGO触发ADC的硬件链路与寄存器配置2.1 先搞清楚触发源怎么选TIM1还是TIM2STM8的ADC触发源选择在ADC_CR2寄存器里TRIG位域可以选内部定时器事件。具体哪个定时器能触发ADC取决于芯片型号。以常见的STM8S103和STM8S105为例TIM1的TRGO事件可以连到ADCTIM2的TRGO也可以但两者在触发输出配置上略有差别。我一般优先选TIM1原因是TIM1是高级定时器它的TRGO输出可以选择多种事件源更新事件、比较匹配、PWM输出等。如果你做电机控制用TIM1的CH4比较事件触发ADC可以精确地在PWM周期的特定时刻采样避开开关噪声。如果只是做固定频率采样用TIM1的更新事件就够了。TIM2也能触发ADC但TIM2的TRGO输出源选择相对少一些一般只用更新事件。如果你的项目里TIM1已经被PWM占用TIM2做采样触发也是完全可行的。配置顺序上先配定时器再配ADC最后开中断。顺序反了容易出现第一次触发丢失或者ADC还没准备好就来了触发信号。2.2 定时器配置算出你要的采样率假设系统时钟是16MHz我要做10kHz的采样率定时器周期怎么算TIM1挂在16MHz主频下不预分频的话计数器每62.5ns加一。10kHz对应周期100us也就是1600个计数。所以ARR设1599预分频器设0。// TIM1初始化更新事件触发ADC采样率10kHz void TIM1_Init(void) { TIM1_TimeBaseInit(0, // 预分频器0即不分频 TIM1_COUNTERMODE_UP, // 向上计数 1599, // 自动重装载值16MHz/(15991)10kHz 0); // 重复计数器高级定时器才用这里设0 // 选择更新事件作为TRGO输出源 TIM1_SelectOutputTrigger(TIM1_TRGOSOURCE_UPDATE); // 使能TIM1开始计数 TIM1_Cmd(ENABLE); }这段代码里最关键的是TIM1_SelectOutputTrigger它把定时器的更新事件映射到TRGO输出线上。更新事件发生在计数器溢出时也就是每1600个计数产生一次。预分频器设为0意味着计数器时钟就是16MHzARR设为1599意味着每1600个时钟周期溢出一次正好10kHz。如果你需要更低的采样率比如1kHz有两种做法加大ARR到15999或者设预分频器为9、ARR为1599。两种做法等效但预分频器设大一点的好处是ARR不会太大计数器分辨率保留得更好。我一般采样率低于5kHz时就加预分频。2.3 ADC配置外部触发模式怎么开ADC这边要配几个关键点通道选择、转换模式、触发源、对齐方式。STM8的ADC_CR1寄存器里有ADON位控制开关ADC_CR2里有TRIG位域选触发源还有EXTTRIG位使能外部触发。// ADC初始化外部触发模式TIM1 TRGO触发 void ADC_Init(void) { // 先开ADC时钟 CLK_PeripheralClockConfig(CLK_PERIPHERAL_ADC, ENABLE); // ADC基本配置单次转换模式右对齐通道0 ADC1_Init(ADC1_CONVERSIONMODE_SINGLE, // 单次转换 ADC1_CHANNEL_0, // 通道0 ADC1_PRESSEL_FCPU_D2, // 预分频ADC时钟8MHz ADC1_EXTTRIG_TIM, // 外部触发选TIM1 DISABLE, // 不使能扫描模式 ADC1_ALIGN_RIGHT, // 右对齐 ADC1_SCHMITTTRIG_CHANNEL0, // 施密特触发器 DISABLE); // 不使能外部触发 }这里有个容易踩坑的地方ADC1_Init的最后一个参数是DISABLE表示初始化时不使能外部触发。但初始化完成后你需要单独调用ADC1_ExternalTriggerConfig来使能外部触发并选择触发边沿。// 初始化完成后单独配置外部触发 ADC1_ExternalTriggerConfig(ADC1_EXTTRIG_TIM, ENABLE);为什么分两步因为标准外设库的设计逻辑是初始化时先不使能触发等你把定时器也配好了再统一开触发避免定时器还没启动就有触发信号进来导致误转换。2.4 中断配置与数据读取ADC转换完成后会产生EOC中断你需要在这个中断里读ADC_DR寄存器。注意读数据之前要清EOC标志否则中断会反复触发。// ADC中断服务函数 INTERRUPT_HANDLER(ADC1_IRQHandler, 22) { // 清EOC标志 ADC1_ClearFlag(ADC1_FLAG_EOC); // 读取转换结果 uint16_t adc_value ADC1_GetConversionValue(); // 把数据存到缓冲区或者做实时处理 adc_buffer[adc_index] adc_value; if (adc_index BUFFER_SIZE) { adc_index 0; buffer_ready 1; // 通知主循环缓冲区满了 } }中断向量号22是STM8S103的ADC中断号不同型号可能不一样查数据手册确认。中断里只做最轻量的操作读数据、存缓冲区、置标志。不要在中断里做浮点运算或者串口打印否则会阻塞后续采样。主循环里检测到buffer_ready标志后再去做数据处理。这样采样和数据处理解耦采样时刻由硬件保证处理慢一点也不会丢采样点。3. 多通道扫描模式下的定时器触发与DMA替代方案3.1 扫描模式怎么配合定时器触发单通道采样用定时器触发很简单但如果你要轮流采4个通道就需要开扫描模式。STM8的ADC扫描模式会在一次触发后依次转换多个通道每个通道转换完都产生EOC中断。配置扫描模式时ADC1_Init的扫描模式参数要改成ENABLE同时用ADC1_ChannelSelect指定起始通道。但STM8的扫描模式有个限制它只能从通道0扫到通道N不能任意选几个不连续的通道。如果你要采通道0、2、5、7扫描模式会从0扫到7中间不需要的通道也会转换浪费时间。我一般这样做如果通道连续比如0到3直接用扫描模式如果通道不连续就每次触发只采一个通道在中断里手动切换通道号。手动切换的代价是每次切换后需要一点稳定时间但STM8的ADC切换通道后第一次转换结果建议丢弃从第二次开始用。// 手动多通道轮询每次触发采一个通道 INTERRUPT_HANDLER(ADC1_IRQHandler, 22) { ADC1_ClearFlag(ADC1_FLAG_EOC); uint16_t val ADC1_GetConversionValue(); // 根据当前通道号存数据 switch (current_channel) { case 0: ch0_buffer[ch0_idx] val; break; case 1: ch1_buffer[ch1_idx] val; break; case 2: ch2_buffer[ch2_idx] val; break; case 3: ch3_buffer[ch3_idx] val; break; } // 切换到下一个通道 current_channel (current_channel 1) % 4; ADC1_ChannelSelect(current_channel); // 启动下一次转换等待定时器触发 ADC1_StartConversion(); }注意这里在中断里调用了ADC1_StartConversion但实际转换启动还是靠定时器触发。ADC1_StartConversion只是把ADC置为等待触发状态。如果定时器触发频率很高中断处理时间必须足够短否则会丢触发。3.2 用DMA搬运ADC数据STM8没有DMA怎么办STM8大部分型号没有DMA控制器这是和STM32最大的区别之一。没有DMA意味着每次ADC转换完都必须用中断或者轮询来读数据。中断方式在采样率10kHz以下问题不大但如果采样率上到50kHz中断频率太高CPU大部分时间都在进出中断主循环几乎跑不动。替代方案有两个一是降低采样率用定时器触发但每N次触发才读一次数据二是用定时器的比较事件触发ADC同时用另一个定时器做数据搬运的软件模拟。第二种做法太绕不推荐。实际项目中如果采样率超过20kHz且需要连续采集我会建议换STM32或者用外部ADC芯片。STM8的定位是低成本控制不是高速数据采集。硬要在STM8上跑50kHz采样中断开销会让你怀疑人生。3.3 采样率与中断开销的实测数据我在STM8S103上跑16MHz主频ADC中断里只做读数据和存缓冲区实测中断服务函数执行时间大约2.5us。这意味着中断频率最高不能超过400kHz否则CPU全在中断里。但实际还要留时间给主循环所以中断频率控制在50kHz以下比较安全。10kHz采样率时中断每100us进一次每次2.5usCPU占用率2.5%完全没压力。50kHz时占用率12.5%还能接受。100kHz时占用率25%主循环会明显变慢。200kHz时占用率50%基本干不了别的了。所以选采样率时先算中断占用率。公式是占用率 中断频率 × 中断执行时间。中断执行时间跟你的中断函数复杂度有关读数据加存数组大概2到3us如果还要做滤波运算可能到10us以上。4. 避坑指南定时器触发ADC的五个血泪教训4.1 第一次触发丢失现象是缓冲区第一个数据是0现象配置完定时器和ADC后启动采样发现缓冲区第一个数据总是0后面的数据正常。原因ADC使能外部触发后需要等待第一个触发信号到来。但如果你在使能触发之前就启动了定时器第一个触发信号可能已经过去了ADC没抓到。或者ADC上电后需要一段稳定时间第一次转换结果不可靠。解决先启动定时器再使能ADC外部触发或者反过来但中间加一个小延时。更稳妥的做法是丢弃前两次转换结果从第三次开始存。// 启动顺序先开定时器延时再开ADC触发 TIM1_Cmd(ENABLE); for(volatile int i 0; i 1000; i); // 等待定时器稳定 ADC1_ExternalTriggerConfig(ADC1_EXTTRIG_TIM, ENABLE); ADC1_StartConversion();4.2 采样率不对实际采样率是设定值的一半现象定时器配的是10kHz但用示波器测ADC中断频率只有5kHz。原因TIM1的更新事件在向上计数模式下溢出时产生一次。但如果你开了重复计数器或者用了中央对齐模式更新事件频率会变。另外ADC的转换时间也要考虑。如果ADC转换时间大于定时器周期触发来了但ADC还在忙这次触发就被忽略了。解决算一下ADC转换时间。STM8的ADC转换时间 采样时间 转换时间。采样时间由ADC_CR1的SPSEL决定转换时间固定14个ADC时钟周期。如果ADC时钟是8MHz采样时间设4个周期总转换时间 (414)/8MHz 2.25us。定时器周期100us远大于2.25us没问题。但如果定时器周期只有1usADC根本来不及转换。4.3 中断里读数据读到旧值EOC标志没清干净现象中断里读到的ADC值偶尔和上一次一样或者数据错位。原因STM8的EOC标志清除时机有讲究。如果你先读ADC_DR再清EOC读到的可能是上一次的数据。正确顺序是先清EOC再读ADC_DR。但有些库函数封装了ADC1_GetConversionValue它内部可能先读后清导致问题。解决手动控制顺序先调ADC1_ClearFlag(ADC1_FLAG_EOC)再调ADC1_GetConversionValue()。另外如果开了过采样或者多次转换模式EOC标志的含义会变仔细看参考手册。4.4 多通道切换后第一个数据不准现象手动切换通道后该通道的第一个采样值明显偏大或偏小。原因ADC内部采样保持电容在切换通道后需要时间稳定。如果切换后立刻启动转换采样电容上的残留电荷会影响结果。解决切换通道后先做一次丢弃转换从第二次开始存。或者切换通道后加一个小延时等采样电容稳定。// 切换通道后丢弃第一次转换 ADC1_ChannelSelect(new_channel); ADC1_StartConversion(); while(!ADC1_GetFlagStatus(ADC1_FLAG_EOC)); ADC1_ClearFlag(ADC1_FLAG_EOC); ADC1_GetConversionValue(); // 丢弃 // 第二次转换才是有效数据4.5 定时器触发和软件触发混用导致时序混乱现象系统里既有定时器触发ADC又在某处调用了ADC1_StartConversion做软件触发结果采样时序乱了。原因STM8的ADC触发源只能选一个要么外部触发要么软件触发。如果你在外部触发模式下调用ADC1_StartConversion它不会立即启动转换而是把ADC置为等待外部触发状态。但如果你在中断里又调了一次可能会覆盖之前的配置。解决统一触发源。要么全用定时器触发要么全用软件触发。如果需要在特定时刻强制采样一次用定时器的比较事件触发不要混用软件触发。5. 用示波器验证采样时序与动态调整采样率5.1 用GPIO翻转法测实际采样率最直接的验证方法在ADC中断里翻转一个GPIO引脚用示波器测引脚频率。频率就是实际采样率。如果和设定值不符检查定时器配置和ADC转换时间。// 在ADC中断里翻转GPIO用于测量实际采样率 INTERRUPT_HANDLER(ADC1_IRQHandler, 22) { GPIO_WriteReverse(GPIOD, GPIO_PIN_0); // 翻转PD0 ADC1_ClearFlag(ADC1_FLAG_EOC); uint16_t val ADC1_GetConversionValue(); adc_buffer[adc_index] val; if (adc_index BUFFER_SIZE) adc_index 0; }示波器测PD0的频率如果是10kHz说明采样率正确。如果只有5kHz检查定时器ARR是不是算错了或者ADC转换时间是不是超了。5.2 动态调整采样率运行时改ARR值有些场景需要在运行时切换采样率比如设备有高速和低速两种工作模式。这时候不需要重新初始化定时器和ADC只需要改TIM1的ARR值。// 动态修改采样率new_arr是新的自动重装载值 void Set_SampleRate(uint16_t new_arr) { // 先停止定时器 TIM1_Cmd(DISABLE); // 修改ARR TIM1_SetAutoreload(new_arr); // 重新使能定时器 TIM1_Cmd(ENABLE); // 等待更新事件让新ARR生效 while(!TIM1_GetFlagStatus(TIM1_FLAG_UPDATE)); TIM1_ClearFlag(TIM1_FLAG_UPDATE); }注意修改ARR后计数器不会立即重载要等下一次溢出。所以改完之后采样率不会马上变会有一个过渡周期。如果对切换时间敏感可以在修改ARR后手动产生一次更新事件。5.3 采样率与数据处理的平衡最后说一个实际项目中的取舍。采样率越高数据量越大处理压力越大。我一般先确定信号带宽采样率取信号最高频率的2.5到4倍。比如测50Hz工频信号采样率200Hz到400Hz就够了没必要上10kHz。如果做音频采集20kHz带宽需要40kHz以上采样率这时候STM8就吃力了。我会建议用外部ADC加SPI接口或者换更高级的MCU。STM8的ADC在10kHz以下采样率时表现很稳超过20kHz就要仔细评估中断开销。我自己的习惯是先在纸上算中断占用率超过15%就考虑降采样率或者换方案。这个习惯帮我省了很多调试时间希望帮到你。本文还有配套的精品资源点击获取