
简介《Atmega8实例全集》是面向嵌入式开发者与AVR初学者的实例资源库围绕Atmel 8位AVR微控制器Atmega8展开覆盖系统初始化、IO输入输出、定时器/计数器、USART与SPI通信、ADC模拟采集、EEPROM存储、中断处理及低功耗休眠等核心开发场景配有可直接导入ICCAVR的C语言工程便于读者从零开始理解芯片特性和编程方法。压缩包共312个文件大小596KB以.c源文件、.prj/.mak工程配置、.hex烧录文件、.dsn Proteus仿真电路以及相关编译辅助文件为主结构紧凑且类型完整。已有594人学习下载。案例包含LCD1602显示、数码管显示、矩阵按键、外部中断、IIC接口读写、数字钟、串口收发、电压读取、一般按键等既适合课堂教学与实验也可作为课程设计、毕业设计或自学入门的参考。结合包内的ICCAVR_atmega8_C文件读者可逐个编译运行并分析仿真结果系统提升对Atmega8实际项目的应用能力。1. 为什么Atmega8至今仍是入门首选先聊点实际的。这几年新出的单片机一个比一个猛ARM Cortex-M主频动辄上百兆RISC-V也来势汹汹可你要真去问那些搞了十几年嵌入式的老工程师他们给新手推荐入门芯片时十个里有六七个还是会说先玩Atmega8。原因不复杂。Atmega8是AVR系列里的经典中端型号8位RISC架构最高16MHz主频2KB SRAM8KB Flash512字节EEPROM三个定时器六个PWM通道一个8通道10位ADC再加上USART、SPI、I2C这些常用外设。这套配置放到今天看不算豪华但恰好卡在一个非常舒服的位置——资源够用但不冗余寄存器数量适中数据手册只有三百多页不像某些芯片一上来就是两千页起步。对新手来说这种“简陋”反而是巨大的优势你能在较短时间内把芯片内部结构摸清楚而不是被复杂的时钟树和电源管理折腾到怀疑人生。再说生态。Atmega8的教程存量极其庞大从国内大学的单片机课程到国外的AVR Freaks论坛从各种开发板的例程到开源社区的DIY项目你几乎能找到任何一个外设的参考代码。这意味着什么意味着你踩坑的时候大概率已经有人替你踩过了而且把解决方案写在了某个不起眼的角落等你发现。这种学习体验说实话是很多新芯片给不了的。我自己的经历也印证了这一点。早年做项目用过不少芯片但真正让我把“看数据手册”这件事练成肌肉记忆的就是Atmega8。它的寄存器少你翻手册的时候不会被海量信息淹没反而能找到那种“哦原来这个位是这样控制的”的清晰感。等你在Atmega8上把GPIO、定时器、中断、串口、ADC都玩明白了再去看ARM或者其他高性能芯片会发现很多概念都是相通的无非是换了个寄存器名字、多了几层封装而已。所以这篇文章不是要教你某个具体项目而是把Atmega8最核心的几个外设模块连同我在实际调试中踩过的坑、总结出的技巧一起梳理一遍。你可以把它当成一份“实例化”的参考手册也可以照着例子自己动手搭电路验证。无论你是刚接触单片机的新手还是想拿Atmega8做点小项目的爱好者这篇内容应该都能给你一些实在的帮助。2. 从最小系统到开发环境搭建别在这些基础环节上翻车2.1 最小系统的三个关键细节先搞定硬件基础。Atmega8要跑起来其实只需要五个元件芯片、电源、晶振、两个电容和一个复位电路。但就是这么简单的电路我见过不少新手翻车主要集中在三个地方。第一是电源去耦。很多初学者直接拿面包板接个5V就完事了结果芯片工作不稳定程序时好时坏。正确做法是在芯片的VCC和GND引脚之间就近放一个100nF的陶瓷电容最好再并一个10uF~100uF的电解电容用来吸收电源上的高频噪声和低频纹波。这个习惯一开始就要养成否则后面做复杂项目时会非常痛苦。第二是晶振电路。如果你使用外部晶振两个22pF左右的负载电容是必须的而且要从晶振引脚直接连到GND走线尽量短。不过这里有个小技巧Atmega8出厂默认使用的是内部8MHz RC振荡器对于很多对时序精度要求不高的实验来说完全可以先用内部振荡器把程序跑起来后面再切换到外部晶振。这能省掉不少前期麻烦。第三个容易忽略的是复位引脚。RESET引脚内部有上拉但如果你直接把它悬空在电磁环境稍差的地方就可能出现意外复位。稳妥起见在RESET和VCC之间接一个10kΩ上拉电阻同时用一个100nF电容到GND能有效滤除干扰。这个配置简单但非常实用。2.2 烧录方式选择与熔丝位警告Atmega8支持ISP在线串行编程、高压并行编程和JTAG部分型号等烧录方式日常使用最常见的是ISP。USBasp是性价比最高的选择十几块钱一个配合AVRdude就能完成烧录。如果你手头有Arduino板也可以把板子刷成ISP模式来给Atmega8烧录本质是一样的都是通过SPI接口操作芯片。但这里我必须重点提醒一件事熔丝位。Atmega8有个特殊机制叫熔丝位Fuse Bits它决定了芯片的时钟源、启动时间、BOOT区大小等关键参数。熔丝位的配置错了轻则芯片无法正常工作重则直接锁死导致ISP无法连接。我早期就干过这种事想把时钟源从内部振荡器切换到外部晶振结果把熔丝位写错了芯片直接“变砖”。后来用高压并行编程器才救回来。所以我的建议是在你对熔丝位还没有完全理解之前尽量不要去动它。如果实在需要修改先记录下芯片当前的熔丝位值再用AVR Studio或AVRdude的图形工具逐位确认后再写入。另外务必使用带有“慢速时钟”选项的编程器防止芯片在低时钟频率下无法响应ISP编程时序。2.3 开发环境与工具链推荐Atmel现在的Microchip官方的IDE经历过多次更名AVR Studio 4 → AVR Studio 5/6 → Atmel Studio 7 → Microchip Studio。目前Microchip Studio依然可以免费下载使用支持GCC编译器调试功能也比较完整适合认真做项目的人。如果你更喜欢轻量级的方案可以用PlatformIO VS Code它对AVR有不错的支持安装方便跨平台。再配合AVRdude命令行直接用Makefile编译烧录体验也别有一番风味。我个人的习惯是Microchip Studio做代码编写和调试然后用命令行工具做批量烧录效率很高。另外强烈建议配一个逻辑分析仪几十块钱的8通道24MHz那种就够用了调试UART、SPI、I2C时序的时候非常好使。早期我调试串口乱码问题时就是靠逻辑分析仪抓到波特率误差过大的真相单靠肉眼和示波器真的很难定位。3. GPIO操作与按键消抖从寄存器到实战细节3.1 寄存器操作的基础逻辑Atmega8的GPIO以端口分组PortB、PortC、PortD每个端口三个关键寄存器DDRx方向、PORTx输出数据/上拉控制、PINx输入读取。这个结构是AVR系列共通的理解了一个端口其他端口就都通了。控制一个引脚做输出需要两步先设置DDRx寄存器把引脚设为输出方向再写PORTx寄存器设置高低电平。比如要让PortB的第0脚输出高电平#include avr/io.h void main(void) { DDRB | (1 PB0); // 设置PB0为输出 PORTB | (1 PB0); // 输出高电平 while (1) { // 主循环 } }注意这里用了位或操作| (1 x)而不是直接赋值DDRB 0x01。原因是直接赋值会把其他引脚的方向一并覆盖在大型项目中容易引发隐蔽的Bug。养成用位操作的习惯是入门阶段就要建立的编码意识。读取输入则是先清零DDRx对应位然后读PINx寄存器。如果你想让引脚处于高阻输入状态保持PORTx清零如果你希望启用内部上拉电阻就置位PORTx对应位。这个上拉电阻大约20kΩ~50kΩ用来接按键非常方便可以省掉外部上拉电阻。3.2 按键消抖硬件和软件两种打法按键可能是GPIO最经典的应用了但机械按键有个绕不开的问题抖动。按键按下去的瞬间由于金属弹片的物理特性引脚电平会在几十毫秒内反复跳变如果不处理一次按压会被误判成多次触发。硬件方案是加一个RC低通滤波器用电阻和电容把跳变沿变缓再用施密特触发器整形。这种方式适合对实时性要求高、又不想占用CPU的场景但会增加元件成本。软件方案更常用。核心思路是“延时二次确认”检测到电平变化后延时10~20ms再读一次如果电平仍然变化才认为是有效按键动作。常见的实现有两种。一种是用_delay_ms(20)直接死等简单但会阻塞CPU另一种是用定时器配合状态机非阻塞处理按键扫描适合需要同时处理其他任务的场景。我早期的项目里图省事用的是死等方案后来发现只要程序里同时跑着数码管动态扫描和串口收发死等20ms的影响会非常明显才下决心改成状态机扫描。贴一个非阻塞按键消抖的核心逻辑uint8_t key_scan(void) { static uint8_t key_state 0; static uint16_t last_time 0; uint8_t key_press 0; uint8_t level (PIND (1 PD2)) ? 1 : 0; // 读取PD2电平 switch (key_state) { case 0: // 初始状态等待按键按下 if (level 0) { key_state 1; last_time timer_get_ms(); } break; case 1: // 已经检测到电平变化进入确认阶段 if (level 0) { if (timer_get_ms() - last_time 20) { key_press 1; // 确认按键有效 key_state 2; } } else { key_state 0; // 抖动或误触回到初始 } break; case 2: // 等待按键释放 if (level 1) { key_state 0; } break; } return key_press; }这段代码的核心是把按键状态分为等待按下、确认有效、等待释放三个阶段用定时器计时而不是死等。实际使用下来20ms的消抖时长在大多数薄膜按键和轻触开关上都表现良好。如果你的按键弹片老化严重可以适当延长到30~40ms。3.3 GPIO项目实例LED呼吸灯之外的进阶玩法网上关于Atmega8的GPIO教程十有八九是点灯但点灯也可以点出层次感。除了普通的LED闪烁我推荐做两个练习一个是利用定时器PWM实现呼吸灯这是理解PWM调制的入门另一个是驱动共阳数码管动态扫描这是理解外部电路驱动能力和时序配合的好例子。我的例子用PortB的PB3OC2输出PWM控制LED亮度。Atmega8的Timer2支持相位校正PWM和快速PWM两种模式配置好寄存器后用一个变量在中断里递增递减就能实现平滑的呼吸效果。这个项目很小但能把定时器、中断、PWM三个核心知识点串起来性价比很高。4. 定时器与中断从原理到实用技巧4.1 Atmega8的定时器家族Atmega8内部有三个定时器/计数器Timer08位、Timer116位、Timer28位。每个的用途侧重点不同刚开始学的时候容易混淆我用一个表格帮大家理清定时器位宽主要特性典型应用Timer08位双通道PWM可做系统时基毫秒延时、简单PWM输出Timer116位最高精度可做输入捕获、比较匹配波形测量、精密延时、伺服舵机控制Timer28位支持异步时钟源可工作在32.768kHz晶振下低功耗RTC、精确守时实际使用中Timer1因为是16位计数范围大做频率测量和宽度测量非常合适。Timer0和Timer2都是8位精度有限但用来做周期性中断再配合状态机已经足够了。4.2 中断系统的三个关键概念中断是Atmega8进阶绕不开的一关而且要真正理解它得先搞懂三个概念。第一是中断向量。AVR的中断向量表固定存放在Flash的起始地址当某个中断事件发生时CPU会跳转到对应的向量地址执行中断服务程序。GCC的头文件已经帮我们定义好了这些向量名比如TIMER1_COMPA_vect、USART_RXC_vect不用记具体地址但你得知道它们的存在。第二是全局中断使能Global Interrupt Enable。通过sei()指令打开。注意如果你只使能了某个外设中断源却忘了调sei()中断永远不会发生。这个坑我在初学阶段踩过症状就是“程序里明明开了定时器中断却怎么都不进中断函数”排查了大半天才发现是忘了全局使能。第三是中断服务程序ISR的注意事项。ISR里要尽量少做事最好只做标志位设置和简单变量更新复杂的处理逻辑放到主循环里。原因是中断会抢占主程序的执行如果在ISR里跑延时函数或者做浮点运算系统的实时性会被严重破坏。这就像你在排队办业务突然来一个VIP客户插队你能简单签个字就完事但如果他占着窗口办半小时业务后面所有人都得等着。4.3 定时器中断实例毫秒时基的实现方法我现在几乎所有项目里都会放一个毫秒时基用定时器中断产生一个“心跳”再用这个心跳去驱动按键扫描、LED闪烁、超时判断等任务。具体做法是配置Timer0为CTC模式比较匹配值为系统时钟/分频/1000这样每过1ms就触发一次比较匹配中断在中断里累加一个毫秒计数器。#include avr/io.h #include avr/interrupt.h volatile uint32_t timer_ms_count 0; ISR(TIMER0_COMP_vect) { timer_ms_count; } void timer0_init(void) { TCCR0 | (1 WGM01); // CTC模式 TCCR0 | (1 CS01) | (1 CS00); // 64分频 OCR0 124; // 8MHz / 64 / 1000 - 1 124 TIMSK | (1 OCIE0); // 使能比较匹配中断 } uint32_t timer_get_ms(void) { return timer_ms_count; }这段代码有两个值得注意的地方。一是OCR0的计算8MHz内部振荡器除以64分频得到125kHz的计数频率再除以1000得到125因为计数从0开始所以减1得到124。如果你的系统时钟不是8MHz这个值要按公式重新算。二是中断处理函数里尽量只做变量累加不要在这里做串口打印、LED翻转这种耗时操作。4.4 输入捕获实战测量信号频率的完整思路Timer1的输入捕获功能可以测量外部信号的频率或脉宽原理是当引脚出现指定边沿上升沿或下降沿时定时器会把当前计数值锁存到ICR1寄存器同时触发捕获中断。连续两次捕获的计数值之差乘以定时器时钟周期就是信号的周期。用这个方法我做过一个简易的频率计测量范围从几十赫兹到几百千赫兹精度还不错。核心流程是初始化Timer1为普通计数模式设置合适的预分频器如果信号频率高预分频小一点防止计数器溢出使能输入捕获中断和边沿选择在中断里读取ICR1值与上一次的差值用系统时钟频率除以差值得到频率。这里有一个容易踩的坑如果信号周期大于定时器的最大计数周期16位计数器会溢出这时需要配合溢出中断来扩展计数范围。否则你计算的差值可能是“混叠”过的得出来的频率完全是错的。我早期做频率测量时就栽在这上面后来加了溢出计数才解决。5. 串口通信与调试技巧解决乱码、丢数据的老大难5.1 USART编程的完整流程USART通用同步异步收发器是Atmega8上最常用的通信外设调试信息打印、与上位机通信、模块交互都离不开它。用USART收发数据标准流程是四步设置波特率、配置帧格式、使能收发、处理收发缓冲。波特率设置是第一步也是新手最容易做错的一步。Atmega8通过UBRR寄存器和UCSRA寄存器的UBRR位共同决定波特率计算公式是UBRR F_CPU / 16 / BAUD - 1。比如8MHz时钟、9600波特率UBRR就是51。如果你的晶振是14.7456MHz这种“非标准”频率算出来的UBRR可能是小数这时候误差会累积时间久了就会出现乱码。下面是一个标准初始化代码void uart_init(uint32_t baudrate) { uint16_t ubrr F_CPU / 16 / baudrate - 1; UBRRH (uint8_t)(ubrr 8); UBRRL (uint8_t)ubrr; UCSRB | (1 RXEN) | (1 TXEN); // 使能收发 UCSRC | (1 URSEL) | (1 UCSZ1) | (1 UCSZ0); // 8位数据无校验1位停止位 }注意UCSRC和UBRRH共用同一个I/O地址通过URSEL位来区分读写目标。这是AVR系列的一个经典设计很多新手在操作时容易弄混导致波特率或帧格式配置不生效。写UCSRC时要记得把URSEL置1写UBRRH时则要清零。5.2 串口乱码的根因排查链路串口乱码几乎是每个玩单片机的人都遇到过的问题。我总结了一套排查思路遇到乱码就按这个顺序查效率很高。第一步确认两边波特率是否一致。这是最常见的原因发端9600收端19200肯定乱码。如果是自己写的上位机检查一下波特率是否被人为改动过。第二步确认时钟频率。去检查芯片的熔丝位配置。如果代码里写的是F_CPU 8000000UL内部8M但熔丝位实际配置的是外部晶振12MHz那么用8MHz算出来的UBRR就完全不对乱码也就顺理成章了。这种问题通常发生在芯片被换过、或者用别人的工程改的时候。解决办法是统一时钟来源要么都改代码里的F_CPU要么改熔丝位让两者保持一致。第三步用逻辑分析仪抓波形。把TX引脚接到逻辑分析仪上看实际输出的波特率是否与设置值一致。如果波形周期明显偏差优先怀疑时钟源不对如果波形周期正确但数据还是乱码就要检查帧格式数据位、停止位、校验位是否匹配。第四步检查双方的地线是否共地。串口通信是单端信号依赖共同的参考地。如果开发板和USB转串口模块分别供电且没有共地信号就会漂移出现间歇性乱码。我自己的经验是把乱码问题当成一个系统工程问题来对待而不是头痛医头。很多时候是时钟源和波特率计算两个问题叠加在一起单看一个方向是找不出真相的。5.3 调试黄金组合printf重定向与硬件断点调试嵌入式程序最原始的方式是烧录后看现象但效率太低了。我建议从一开始就在Atmega8工程里做两件事重定向printf到串口和学会用调试器的硬件断点。printf重定向的方法在GCC工具链下很成熟自己实现uart_putchar然后把stdout指向一个自定义的FILE流之后就可以像PC编程一样用printf打印调试信息了。注意AVR的库对浮点格式支持会显著增加Flash占用所以尽量避免在printf里用%f。如果一定要打印浮点数可以先用整型运算放大一千倍再打印。硬件断点则是用Microchip Studio里内置的调试器如AVR Dragon、Atmel-ICE实现的配合JTAG或debugWIRE接口。断点能让你停在任意一行C代码检查寄存器、变量的实时值这对定位那些“逻辑上没错但就是不工作”的问题非常有用。不过要注意某些低成本的USBasp编程器不支持在线调试只支持烧录要调试还得用专门的调试器。6. ADC采样与传感器读取让单片机感知世界6.1 ADC寄存器配置的要点Atmega8内置8通道、10位逐次逼近型ADC参考电压可选内部2.56V、AVCC或者外部AREF引脚。使用ADC的流程很固定设置ADMUX选择参考电压和通道、设置ADCSRA使能ADC并配置分频系数、触发转换、等待转换完成并读取结果。uint16_t adc_read(uint8_t channel) { ADMUX (1 REFS0) | (channel 0x07); // AVCC为参考电压选择通道 ADCSRA | (1 ADSC); // 启动转换 while (ADCSRA (1 ADSC)); // 等待转换完成 return ADCW; // 读取10位结果 }有一个很多人忽略的点ADC时钟频率不能太高。数据手册建议ADC时钟在50kHz~200kHz之间以保证采样精度。转换一次大约需要13个ADC时钟周期如果ADC时钟是125kHz一次转换也就1ms左右完全够用。如果系统主频是16MHzADC预分频至少要选128分频也就是ADC时钟125kHz。这个参数没配好采样值会跳动得厉害后期滤波都救不回来。还有一个细节通道切换后最好做一次“空转换”或加一小段延时让ADC内部采样保持电容充分充电。否则切换到高阻抗信号源时第一次采样的结果可能偏差较大。6.2 使用内部2.56V基准的注意事项Atmega8可以选择内部2.56V作为ADC参考电压这个配置在供电电压波动时特别有用。但要注意内部基准的初始精度并不高典型误差在±1%左右对传感器测温和电池电压监测来说够用但如果你要做精确电压表就必须使用外部精密参考源比如TL431或者专门的电压基准芯片并且用万用表标定。另外如果ADC输入信号阻抗很高比如某些气体传感器输出端没有跟随器建议加一个运放做缓冲或者在采集端并联一个0.1uF电容让信号源能快速充满采样电容。否则采样值会不稳定且容易受线路电容影响。6.3 传感器读取实例光敏电阻与热敏电阻的标定思路光敏电阻和热敏电阻都是阻值随外界物理量变化的器件用ADC读取时通常构成分压电路。以光敏电阻为例光敏电阻一端接VCC另一端串联一个固定电阻到GND中点接ADC引脚。光线变强时光敏电阻阻值下降或上升取决于型号ADC值随之变化。但这里有个坑不同型号的光敏电阻亮阻和暗阻范围差异很大。我建议拿到器件后先实测两个极限值强光下的ADC值和全黑环境下的ADC值然后把这些原始值映射到0~100%的亮度百分比。这个过程叫标定。不做标定直接用ADC原始值去判断“亮”“暗”阈值设得再准也白搭因为不同板子、不同批次器件的原始值会有明显差异。热敏电阻同理NTC热敏电阻的阻值变化是非线性的可以用公式R R25 * exp(B * (1/T - 1/298.15))逼近B值是器件手册里标定好的材料常数。不过在要求不高的场景下我通常直接查表法——用一个温度表标定几个温度点把ADC值和温度的对应关系建一个表格程序里用线性插值来算。这种方法简单有效而且避开了复杂的对数运算。6.4 用均值滤波压制ADC噪声的实操方案ADC的原始采样值通常会有几个字LSB的随机抖动如果直接把原始值拿来用LED亮度会出现肉眼可见的闪烁电机控制也不稳定。解决办法是软件滤波。最简单有效的是滑动平均滤波维护一个环形缓冲区每次采样后计算缓冲区平均值。窗口大小一般取8~32窗口太大响应变慢窗口太小滤波效果有限。对一个10位ADC窗口取16个样本效果已经相当不错。更进一步可以采用中位值平均滤波法也就是去掉一个最大值和一个最小值剩下的取平均。这种滤波方式能同时抑制随机噪声和偶发脉冲干扰。AVR的Flash和SRAM虽然不大但实现这种滤波完全够用。我一般会在代码里用移位运算代替除法除以16就是右移4位避免编译器生成调用库函数的慢速除法代码。7. PWM输出与电机控制从波形到真实世界的驱动7.1 快速PWM与相位校正PWM的区别Atmega8的定时器都支持PWM输出但有两种模式需要区分快速PWMFast PWM和相位校正PWMPhase Correct PWM。快速PWM的特点是计数器只从0向上计数到达TOP值后直接清零重新开始输出频率较高可以达到定时器时钟/256×分频的水平适合做LED亮度调节、DC电机调速这类对相位连续性不敏感的场景。缺点是会产生较大的开关噪声和谐波因为波形上升和下降沿不对齐。相位校正PWM的特点是计数器先向上计数、再向下计数到达TOP后折返形成一个三角波。输出波形在时间轴上是对称的相位连续性好适合音频输出或者需要低失真的场合。缺点是同样的分频下输出频率只有快速PWM的一半。实际选型原则很简单一般项目用快速PWM追求波形质量时用相位校正PWM。7.2 直流电机调速PWM频率与驱动电路的两难选择用PWM给直流电机调速有两个问题要同时考虑PWM频率和驱动能力。PWM频率太低电机会产生明显的“嗡嗡”声转速不稳甚至会出现阶跃跳动频率太高电机电感会让电流纹波变得平滑但驱动芯片的开关损耗增加发热上升。常见的经验范围是10kHz~30kHz我一般取15kHz左右既能避开人耳敏感频段又不会让MOS管太烫。驱动能力方面Atmega8的引脚输出电流只有40mA直接驱动小型玩具电机勉强可以但稍大一点的电机就会把芯片玩坏。建议用专用的电机驱动芯片如L293D、TB6612或者自搭H桥电路电机供电单独拉一路不要和芯片共用同一个稳压源。另外电机两端务必反接一个续流二极管或者两个一个吸收正反向能量否则关断瞬间的反电动势会击穿驱动管。我早期做小车的教训是驱动芯片使能脚和PWM脚接反导致电机低速启动时毫无响应当时还以为是程序问题查了大半天。后来仔细对照数据手册才发现使能脚必须拉高后PWM信号才能真正到达输出端。这种小问题遇到一次就会记住一辈子。7.3 舵机控制实例50Hz周期和1~2ms脉宽的微妙关系舵机可能是PWM应用里最适合练手的项目因为它对时序要求精确逻辑却很简单标准舵机需要一个周期20ms50Hz的PWM信号高电平脉宽在1ms~2ms之间对应0°~180°的转角。Atmega8的Timer1是16位定时器非常适合做舵机控制。我用Timer1的快速PWM模式设定TOP值为20000对应20ms周期然后改变比较匹配值来调整脉宽。比如要使舵机转到中间90°脉宽1.5ms比较值就是1500。void servo_init(void) { DDRB | (1 PB1); // OC1A输出 ICR1 20000; // 20ms周期 TCCR1A | (1 WGM11) | (1 COM1A1); TCCR1B | (1 WGM13) | (1 WGM12) | (1 CS11); // 快速PWM8分频TOPICR1 } void servo_set_angle(uint8_t degree) { OCR1A 1000 (uint16_t)degree * 1000 / 180; // 1~2ms 对应0~180度 }注意舵机不要长时间堵转或者频繁超范围转动电流会明显增加舵机内部齿轮也有损坏风险。调试时建议先不加负载用小角度测试确认方向和行程正确后再装到机构里。8. ATmega8的进阶玩法让这个老芯片继续发光8.1 用I2C总线挂载多个传感器ATmega8只有硬件I2C接口但功能很完整标准模式100kHz快速模式400kHz。我常用它连接温度传感器、气压传感器、OLED显示屏一条总线上挂载四五个从设备轻轻松松。关键是每个设备要有不同的从机地址硬件设计时就要规划好地址引脚的电平。使用I2C时有个常见的坑总线上拉电阻。I2C规范要求在SCL和SDA线上各接一个2.2kΩ~4.7kΩ的上拉电阻到VCC。如果你用的是模块化的传感器许多模块本身已经带了上拉电阻再外接上拉就会导致阻值并联偏小信号边沿变缓甚至无法通信。最稳妥的做法是查清模块手册确认模块是否自带外接上拉电阻然后决定你这边是否还需要额外加电阻。8.2 利用中断和睡眠实现低功耗待机用电池供电的项目低功耗是刚需。ATmega8支持多种休眠模式空闲模式Idle、ADC噪声抑制模式、掉电模式Power-down和待机模式Standby等。掉电模式下除了外部中断和看门狗唤醒其余外设全部停止电流能降到1uA以下这是绝大多数电池供电项目的主力模式。实现低功耗待机的思路是主循环执行完任务后进入休眠等待外部事件比如按键按下、传感器报警信号触发外部中断唤醒CPU。唤醒后执行任务再回到休眠循环往复。这里有个经验技巧按键唤醒时要特别注意因为机械按键抖动可能会触发多次外部中断虽然能被消抖逻辑兜住但会频繁唤醒CPU增加功耗。最好在按键上并联一个100nF电容滤除高频抖动减少误唤醒。8.3 打破Flash和RAM瓶颈的实用策略ATmega8的8KB Flash和2KB SRAM放在今天的应用场景里确实紧张。优化手段有几种。一是把常量数据用PROGMEM放到Flash而不是SRAM比如显示用的中文字库、传感器校准表二是避免使用递归和大型局部数组尽量用全局变量复用缓冲区三是优化编译选项Flash占用吃紧时用-Os追求速度时用-O2。另外有一些常用的算法库可以直接移植比如整数版本的快速PID控制器完全能跑得动。我做过一个温控项目用8MHz时钟、Timer1做100ms采样周期PID运算加上数码管刷新加上串口输出全都在ATmega8上顺畅运行Flash和RAM还有富余。9. 写在最后从ATmega8出发但别被它限制ATmega8是一块非常经典的芯片但它的经典不在于性能而在于它把“单片机”最基本的形态展示得足够清晰。玩通了这块芯片再去学习STM32、ESP32、树莓派Pico你会发现很多概念只是换了壳GPIO还是GPIO定时器还是定时器中断还是中断通信接口也大同小异。我个人的建议是入门阶段用ATmega8把基础打牢但不要停留在“只会点灯”的程度。试着用它做一个小而完整的项目比如一个温控风扇、一个可以设置时间的电子钟、一台带遥控的智能小车。完成这些项目的过程远比背数据手册重要得多。最后分享两个小技巧。第一遇到问题先查数据手册再上网搜顺序不要反数据手册里的信息最权威第二源码注释里清晰记录硬件接线尤其是引脚编号和信号方向这样隔几个月再翻出来还能快速回忆起来。希望这篇内容对你有帮助也欢迎在评论区聊聊你自己用ATmega8做过哪些有意思的东西。本文还有配套的精品资源点击获取