
简介面向ATtiny1616频率测量与输入捕获应用的单片机及嵌入式开发者这份资源将官方数据手册与一套可编译的Atmel Studio工程集成在一起帮助解决事件触发中断、定时器计数值读取及频率反推等实现问题。工程通过PWM模块生成方波信号接入事件系统触发中断记录定时器计数值并依据系统主频换算频率同时利用RTC定时1秒把结果经串口输出适合学习EVSYS、TCB、RTC与USART外设协同开发的场景。压缩包共有126个文件以C源文件.c/.h为主体还包括编译生成的目标文件、ELF/HEX烧录文件、PDF数据手册以及工程配置文件整体大小4.82MB目录便于查询。目前已有480人学习适合希望在官方示例基础上快速验证芯片外设功能或搭建频率计数方案的开发者。1. ATtiny1616 频率测量为什么比你想的更依赖事件系统看到“输入捕获”四个字大多数工程师第一反应是定时器通道的捕获寄存器但ATtiny1616的输入捕获路径完全不是这个逻辑。它的TCB定时器不直接挂在引脚上而是通过EVSYS事件系统把引脚边沿路由进捕获单元连PWM和RTC都要在这个事件链路里排队。整个测量精度不取决于定时器分频多细而取决于你的事件通道配置、TCB时钟选择以及读取CCMP的时机。这个从STM32F4转过来的人最容易踩坑在F4上直接配置TIMx_CCMR就能捕获而到了AVR单片机上必须先建事件通道再给TCB挂用户最后在中断里处理捕获值。下面这个工程恰好把这套流程完整走了一遍用PWM生成固定频率方波接入事件引脚让TCB记录周期计数再经RTC每秒定时、USART串口打印。对做频率计、信号采集或嵌入式入门教学的人很有参考价值。2. TCB 输入捕获的计数模型与主频误差来源在动代码之前先立住一个模型TCB0在输入捕获模式下是一个16位自由计数器它的时钟源不是事件本身而是你选定的CLK_TCB。每次EVSYS送来的有效边沿都会把当前CNT锁进CCMP并触发捕获中断配置成“捕获后清零”时CCMP直接就是两个有效边沿之间的时钟脉冲数。于是被测频率可以表达为[ f_{signal} \frac{CLK_TCB}{CCMP} ]如果主频CLK_PER是20MHzTCB时钟源不分频则计数分辨率50ns。这个公式看起来简单实际工程里有三个误差源要处理EVSYS边沿检测与TCB捕获动作之间有一个路由延迟通常固定但会随事件通道选择不同而变化CCMP读出时机如果晚于下一次捕获发生读到的值已经属于后一个周期这是中断延迟造成的相位偏差最后是TCB的CNT清零与捕获锁存谁先谁后的问题正确配置下锁存在先、清零在后但软件在中断里再读一次CCMP时硬件可能已经处理完。2.1 EVSYS 事件通道的建立与边沿去向ATtiny1616的EVSYS支持多路事件生成器映射到多路用户。配置时先选定通道的事件源比如PA3引脚上升沿再把该通道分配给TCB0。不要把事件生成器直接接到TCB用户上中间必须有一个通道号因为通道可以额外做边缘检测和滤波。常见做法是在Atmel START的Event System里新建一个Channel输入源选择引脚边沿再在TCB0的用户映射里选择这个Channel。这一步决定了后续所有中断标志是否生效顺序颠倒就是“信号已经进芯片但定时器毫无反应”。TCB的时钟源选择也要在这里想清楚。Atmel START的tcb_init默认把TCB0时钟源设置为CLK_PER好处是计数分辨率等于主频周期误差最小。如果要降低计数功耗可以选CLK_PER/2但换算公式会变成f (20000000 / 2) / raw_cap。工程里我一般保持默认不分频把分频留给PWM去做这样TCB始终能以最高分辨率工作不会在低频测量时白白损失一位精度。2.2 从 CCMP 反推信号频率以工程里的TCB为例读取和换算代码在application层调用核心逻辑如下/* 用捕获值计算频率 */ uint32_t freq_hz 0; uint16_t raw_cap; cli(); /* 关中断防止读到一半被更新 */ raw_cap TCB0.CCMP; sei(); if (raw_cap 0) { /* 主频20MHzTCB时钟不分频 */ freq_hz 20000000UL / raw_cap; } printf(f%lu Hz\r\n, (unsigned long)freq_hz);这里cli/sei是为了确保读取CCMP时不会发生新的捕获事件否则高8位和低8位可能来自两次捕获的不同周期。使用Atmel START生成的函数时直接在中断里读TCB0_CCMP主循环里只使用一个volatile全局变量副本也是同样的道理。虽然AVR的16位寄存器读取有硬件原子性保证但在捕获周期远小于中断处理时间时仍建议用临界区保护。2.3 主频上限与低频溢出的边界TCB是16位计数器最大捕获值65535所以单周期测量合法区间在[2, 65535]当信号频率过低时捕获值溢出CCMP仍会更新但结果失真。以20MHz主频为例可测频率范围约305Hz65535×50ns3.2767ms周期到10MHz左右周期5个计数步长再高一个周期只计2~3个脉冲误差无法接受。如果你要测市电50Hz直接输入捕获是溢出的这就是为什么工程里把RTC 1秒定时加进来——每秒统计捕获中断次数而不是只读CCMP。下面这张表是常见主频下的参考边界。CLK_TCB分辨率最大可测周期对应最低频率20MHz50ns3.2767ms约305Hz10MHz100ns6.5535ms约152Hz1MHz1us65.535ms约15.3Hz这里给一个调试建议改变主频或TCB时钟分频后最低频率边界会等比变化但高频精度不会变好。真正要扩展低频应该走RTC计数或把TCB配置成周期测量模式后面第四章会讲。3. Atmel START 工程里每个文件到底在干什么拿到这个工程先别急着打开代码看一遍文件结构能省下半天排查时间。MyProject.atsln是VS的工程入口atmel_start_config.atstart是外设配置存档.atsuo保存的是调试器和芯片选择信息。真正能改逻辑的代码在pwm_basic.c、usart_basic.c、evsys.c、rtc.c、tcb.c和application.c里。其中driver_init.c只负责按顺序调用各模块初始化只要用Atmel START重新生成配置这个文件会被覆盖手动改的寄存器代码要写到各模块的.c里。3.1 四个外设模块的职责边界频率测量的信号链需要四个外设协同同一个信号要经过PWM生成、事件路由、TCB捕获、RTC定时最后USART输出每一环节都独立成文件。下面表格是这几个模块的职责。文件外设关键作用pwm_basic.cTCA0产生固定频率方波作为被测信号evsys.cEVSYS将引脚事件路由到TCB0tcb.cTCB0捕获事件锁存计数值rtc.cRTC产生1秒周期用于定时打印usart_basic.cUSART0打印频率值这种文件划分不是随便分的它严格对应Atmel START的一个页面配置。改PWM只动pwm_basic.c改捕获模式只动tcb.c其他文件里的初始化顺序被driver_init.c固定想加一个外设也必须在这里注册否则即使配置工具生成出来了也不会执行。3.2 PWM 初始化的关键参数周期寄存器pwm_basic.c里PWM使用TCA0的单斜率模式输出引脚频率由PER和CMP0决定。一个常见误区是把CMP0当占空比百分比来写实际上它是占空比的计数值。要输出1kHz、50%占空比主频20MHzTCA0不分频时周期计数20000则PER19999CMP010000。/* TCA0 输出 1kHz 50% 方波 */ TCA0.SINGLE.PER 19999; /* 周期 20MHz / 1000Hz - 1 */ TCA0.SINGLE.CMP0 10000; /* 50% 占空比 */ TCA0.SINGLE.CTRLA | TCA_SINGLE_ENABLE_bm;注意TCB捕获的是这个PWM输出信号所以PWM频率就是被测频率。调试时改PER数值串口打印的频率值必须跟着变否则问题不在PWM而在事件链路。CMP0的值在50%占空比时正好是PER/2如果是25%占空比就改成PER/4但注意不是所有频率都能整除建议在计算完成后再写寄存器避免周期翻转产生一个不完整脉冲。3.3 事件系统初始化通道号与用户号必须配对evsys.c里会看到类似下面的配置/* EVSYS 通道0由引脚事件驱动 */ EVSYS.CHANNEL0 EVSYS_CHANNEL0_PIN0_gc; /* 宏名以实际引脚选择为准 */ /* TCB0 作为通道0的用户 */ EVSYS.USER0 EVSYS_USER0_CHANNEL0_gc;这段代码的核心是EVSYS_CHANNEL0_PIN0_gc这类事件源宏。不同引脚对应不同编码Atmel START生成后不要手动改除非你确定知道它在数据手册里的事件生成器编号。通道号从0到7用户号要和TCB0的实际映射一致写错的结果是硬件完全收不到事件但程序不报错只能靠串口或调试器观察TCB0是否计数。如果要在引脚边沿之前加滤波通道配置里还有一个去毛刺位但一打开它就会引入额外的边沿延迟频率测量场景下不建议开。3.4 USART 和 RTC 的初始化顺序usart_basic.c把波特率折算到BAUD寄存器常用的值是9600和115200。ATtiny1616的USART有标准帧格式和自动波特率校准工程里用printf重定向后注意发送字符串前等待数据寄存器空否则串口输出乱码。rtc.c的配置走的是RTC的周期中断分频后让RTC每秒触发一次中断。这个中断里只做一件事把当前捕获值换算出来放进待发送缓冲区。千万不要在RTC中断里直接调用printf因为RTC中断可能打断正在进行的USART发送造成字符撕裂。正确做法是置一个标志位主循环检测到后统一打印。4. PWM 触发信号到串口输出频率值的完整链路4.1 信号注入方式内部连线还是外部飞线这个工程有两种接法。第一种是内部事件路由PWM生成器输出事件直接连到EVSYS不经过物理引脚适合纯功能验证。第二种是把PWM引脚与事件输入引脚用杜邦线短接更接近真实测量场景。工程里的默认配置走的是第二种PWM输出接到PA0PA0内部事件源映射到EVSYS通道0TCB0在通道0上捕获上升沿。外部接法要注意PWM输出模式是否为推挽以及被测信号是否要经过电阻分压ATtiny1616引脚耐压只有VDD0.5V超过会烧引脚。从信号链上看这一环节最容易忽略的是“触发引脚”的电平状态。如果外部信号在开机瞬间是高电平TCB捕获的是下降沿如果信号是低电平捕获的是上升沿。工程里选择了上升沿捕获就意味着被测方波必须从低到高跳变PWM初始化时CMP0小于PER/2能保证上升沿清晰占空比太大时上升沿可能被误判成毛刺。4.2 捕获中断里的读取顺序TCB0中断标志在事件边沿置位读取CCMP前先确认INTFLAGS。Atmel START生成的TCB0_ISR默认检查捕获标志但如果你在多个中断源共用同一个向量要处理的是捕获标志而不是溢出标志。volatile uint16_t g_last_capture; volatile uint8_t g_capture_ready; ISR(TCB0_INT_vect) { if (TCB0.INTFLAGS TCB_INTFLAGS_CAPT_bm) { TCB0.INTFLAGS TCB_INTFLAGS_CAPT_bm; /* 写1清标志 */ g_last_capture TCB0.CCMP; g_capture_ready 1; } }这里的顺序是先读CCMP再清标志还是先清标志再读CCMP实际工程里我一般先读后清。捕获值在标志置位时已经锁存清标志不会影响它。反过来如果先清标志万一在清完标志后、读取指令前又来了一个捕获事件标志会再次置位而你读到的CCMP已经是第二次捕获的周期数据就错位了。注意清标志必须在读取CCMP之后否则在两条指令之间进来的新捕获事件会覆盖CCMP导致数据错位。4.3 RTC 1秒定时与频率打印RTC配置成每秒中断一次中断里把g_capture_ready的动作放主循环处理同时把全局频率标志置位。主循环里看到g_print_ready为1就打印。我们不直接打印上次的频值而是打印最近几个捕获值的平均值这样能把抖动压下去。volatile uint8_t g_print_ready; volatile uint32_t g_freq_sum; volatile uint8_t g_freq_count; ISR(RTC_CNT_vect) { RTC.INTFLAGS RTC_OVF_bm; /* 清除中断标志 */ g_print_ready 1; } int main(void) { system_init(); while (1) { if (g_capture_ready g_print_ready) { uint32_t f 20000000UL / g_last_capture; char buf[16]; itoa(f, buf, 10); usart_print_string(buf); usart_print_string(\r\n); g_capture_ready 0; g_print_ready 0; } } }这段代码里usart_print_string需要自己实现或使用Atmel START的usart_write封装。打印频率的时机由RTC的1秒中断决定因此无论信号频率怎么变串口输出节奏始终稳定在一秒一行这也是为什么要在RTC中断里而不是捕获中断里置打印标志。如果你的串口助手看到的是不规律爆刷多半是把这个标志逻辑放错了地方。4.4 测量值跳变的排错顺序如果打印结果和预期频率偏差很大按这个顺序排查。第一看波特率和时钟源是否匹配20MHz主频下9600波特率的BAUD寄存器值带小数直接用整型计算会有偏差但误差在0.2%以内第二看TCB时钟分频tcb_init里一旦选了CLK_TCB_DIV2_gc换算公式必须同步改第三看PWM输出是否稳定用示波器量PA0的频率如果它本身就不对后面怎么补偿都没用。最常见的情况是中断读取顺序写反或没清标志导致串口打印的值只有实际频率的一半或两倍。5. 用二次捕获与软件滤波把测量范围拉大两档5.1 二次捕获差值的实现先看一个具体技巧用TCB的连续两次捕获值相减代替单次CCMP直接除。前面代码直接取CCMP但如果信号占空比不是50%或者捕获边沿选择了上升沿而PWM输出相位抖动单次周期计数会有几个计数的偏差。更稳的写法是维护一个3元素环形缓存每次捕获存CCMP取最近两次的差值再算频率uint16_t cap_buf[3]; uint8_t cap_idx 0; /* ISR里做环形缓冲 */ cap_buf[cap_idx] TCB0.CCMP; cap_idx (cap_idx 1) % 3; /* 主循环用最后两个的差值 */ uint16_t period (cap_buf[(cap_idx1)%3] - cap_buf[(cap_idx2)%3] 65536) % 65536;这个处理在内部捕获模式下看似多余但换成真实外部信号时就很有用。TCB捕获的是边沿到边沿的计数值如果信号上叠加了毛刺单次CCMP会出现尖峰而差值法能把跳变周期过滤掉。差值法还能消除TCB捕获后清零时序带来的固定偏差因为两次捕获的相位误差相等相减后自然抵消。5.2 用RTC扩展低频量程再拉大测量范围的方法是交给RTC计数。当输入频率低于TCB溢出阈值时CCMP已经失去意义不要在CCMP上硬修。改成在TCB捕获中断里给一个uint32_t overflow_count加一然后RTC每秒中断时统计总次数这就是频率值。配合TCB事件捕获和溢出标志20MHz主频下最低能测到1Hz以内。验证时用示波器量PWM输出直接对比串口打印的频率值。先把PWM调到1kHz打印稳定在1000附近再改成50kHz如果打印值突然跳到100kHz检查是不是事件源选成了双边沿。边界条件下建议用信号发生器注入标准1kHz、10kHz方波而不是依赖PWM自测这样可以排除PWM外设自身的误差。本文还有配套的精品资源点击获取