简介:面向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是20MHz,TCB时钟源不分频,则计数分辨率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) { /* 主频20MHz,TCB时钟不分频 */ 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主频为例,可测频率范围约305Hz(65535×50ns=3.2767ms周期)到10MHz左右(周期5个计数步长,再高一个周期只计2~3个脉冲,误差无法接受)。如果你要测市电50Hz,直接输入捕获是溢出的,这就是为什么工程里把RTC 1秒定时加进来——每秒统计捕获中断次数,而不是只读CCMP。下面这张表是常见主频下的参考边界。
| CLK_TCB | 分辨率 | 最大可测周期 | 对应最低频率 |
|---|---|---|---|
| 20MHz | 50ns | 3.2767ms | 约305Hz |
| 10MHz | 100ns | 6.5535ms | 约152Hz |
| 1MHz | 1us | 65.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.c | TCA0 | 产生固定频率方波,作为被测信号 |
| evsys.c | EVSYS | 将引脚事件路由到TCB0 |
| tcb.c | TCB0 | 捕获事件,锁存计数值 |
| rtc.c | RTC | 产生1秒周期,用于定时打印 |
| usart_basic.c | USART0 | 打印频率值 |
这种文件划分不是随便分的,它严格对应Atmel START的一个页面配置。改PWM只动pwm_basic.c,改捕获模式只动tcb.c,其他文件里的初始化顺序被driver_init.c固定,想加一个外设也必须在这里注册,否则即使配置工具生成出来了也不会执行。
3.2 PWM 初始化的关键参数:周期寄存器
pwm_basic.c里,PWM使用TCA0的单斜率模式,输出引脚频率由PER和CMP0决定。一个常见误区是把CMP0当占空比百分比来写,实际上它是占空比的计数值。要输出1kHz、50%占空比,主频20MHz,TCA0不分频时周期计数20000,则PER=19999,CMP0=10000。
/* 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输出接到PA0,PA0内部事件源映射到EVSYS通道0,TCB0在通道0上捕获上升沿。外部接法要注意PWM输出模式是否为推挽,以及被测信号是否要经过电阻分压,ATtiny1616引脚耐压只有VDD+0.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_idx+1)%3] - cap_buf[(cap_idx+2)%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外设自身的误差。
本文还有配套的精品资源,点击获取