引言:什么是AVR单片机及其重要性

AVR单片机是由Atmel公司(现为Microchip Technology的一部分)开发的一系列8位微控制器,基于增强的RISC架构。它因其高性能、低功耗和易于编程的特点,在嵌入式系统和物联网项目中广受欢迎。对于初学者来说,AVR单片机是进入嵌入式开发的理想起点,因为它有丰富的社区支持、开源工具链(如AVR-GCC),以及像Arduino这样的流行平台(基于AVR)。本指南将从零基础开始,逐步讲解核心技术,包括硬件架构、开发环境搭建、编程技巧,并通过实战代码示例帮助你快速上手。

AVR单片机的核心优势在于其哈佛架构(程序存储器和数据存储器分离),支持高达20MIPS的执行速度(在16MHz时钟下)。常见型号如ATmega328P(Arduino Uno的核心)和ATtiny系列,适用于从简单LED闪烁到复杂机器人控制的各种应用。通过本指南,你将学会如何选择合适的AVR芯片、搭建开发环境、编写高效代码,并进行调试。让我们从基础开始,逐步深入。

第一部分:AVR单片机的硬件架构基础

1.1 AVR的核心组件概述

AVR单片机的硬件架构基于RISC(精简指令集计算机)设计,具有32个通用工作寄存器,这些寄存器直接映射到ALU(算术逻辑单元),使得指令执行非常高效。核心组件包括:

  • CPU核心:支持130多条指令,大多数在单时钟周期内完成。
  • 存储器
    • Flash程序存储器:用于存储固件代码,通常为几KB到几百KB。
    • SRAM数据存储器:用于运行时变量存储,容量从512字节到几十KB。
    • EEPROM:非易失性存储,用于保存配置数据,如用户设置。
  • 输入/输出端口(GPIO):每个端口(如PORTB、PORTC)有多个引脚,可配置为输入或输出,支持上拉电阻。
  • 定时器/计数器:如Timer0、Timer1,用于精确计时、PWM生成和事件计数。
  • ADC(模数转换器):将模拟信号(如传感器读数)转换为数字值,通常为10位分辨率。
  • 通信接口:包括UART(串口通信)、SPI和I2C,用于与其他设备连接。
  • 中断系统:支持外部中断和内部中断,允许实时响应事件。

这些组件通过内部总线连接,形成一个完整的微型计算机系统。理解这些是编程的基础,因为代码直接操作这些硬件寄存器。

1.2 选择合适的AVR芯片

对于入门,推荐从ATmega328P开始(32KB Flash,2KB SRAM,23个GPIO引脚)。如果空间有限,选择ATtiny85(8KB Flash,512B SRAM,6个引脚)。评估标准:根据项目需求选择引脚数、存储大小和外设。例如,LED项目用ATtiny,复杂项目用ATmega。

第二部分:搭建开发环境

2.1 硬件准备

  • 开发板:Arduino Uno(内置ATmega328P,USB接口,易于上手)或裸芯片+面包板。
  • 编程器/调试器:USBasp(廉价AVR编程器)或AVR ISP MKII。对于Arduino,直接用USB。
  • 其他工具:面包板、跳线、LED、电阻(220Ω)、杜邦线、万用表。

2.2 软件工具链安装

AVR开发有两种主要方式:使用Arduino IDE(简化版)或纯AVR-GCC(更灵活)。我们从Arduino开始,逐步过渡到原生AVR编程。

Arduino IDE安装(适合零基础)

  1. 下载Arduino IDE从官网(https://www.arduino.cc/en/software)。
  2. 安装后,连接Arduino Uno到电脑USB。
  3. 在IDE中选择板型:工具 > 板 > Arduino Uno。
  4. 安装AVR驱动(Windows可能需要CH340驱动,如果是克隆板)。

纯AVR-GCC工具链(高级,推荐后期使用)

  • Windows:安装WinAVR(包含AVR-GCC、AVRDUDE)。
  • Linux/Mac:使用包管理器,如sudo apt install avr-gcc avr-libc avrdude(Ubuntu)。
  • 编辑器:VS Code + PlatformIO插件,或Atmel Studio(Microchip官方IDE)。

验证安装:打开终端,运行avr-gcc --version,应显示版本信息。

2.3 烧录程序到芯片

使用AVRDUDE命令烧录HEX文件。例如,烧录到ATmega328P:

avrdude -c usbasp -p m328p -U flash:w:main.hex

这会将编译后的代码写入Flash存储器。

第三部分:AVR编程基础

3.1 编程语言:C语言为主

AVR编程主要用C语言(或C++),因为高效且接近硬件。避免汇编,除非优化关键部分。AVR-GCC编译器将C代码转换为机器码。

基本程序结构

一个AVR程序(称为固件)包括:

  • 头文件:包含硬件定义,如#include <avr/io.h>
  • 主函数:int main(void) { ... }
  • 初始化:配置端口、时钟等。
  • 主循环:while(1) { ... },无限循环执行任务。

3.2 GPIO操作:点亮LED

GPIO是最基本的外设。端口有三个寄存器:

  • DDRx:数据方向寄存器(1=输出,0=输入)。
  • PORTx:输出数据寄存器(高电平=1,低电平=0)。
  • PINx:输入数据寄存器(读取引脚状态)。

实战代码:点亮PB0引脚的LED(ATmega328P)

假设LED连接到PB0(Arduino的D8),通过220Ω电阻接地。

#include <avr/io.h>  // 包含I/O寄存器定义
#include <util/delay.h>  // 包含延时函数

int main(void) {
    // 配置PB0为输出
    DDRB |= (1 << PB0);  // 设置PB0位为1,输出模式
    
    while (1) {
        // 点亮LED(高电平)
        PORTB |= (1 << PB0);
        _delay_ms(500);  // 延时500ms
        
        // 熄灭LED(低电平)
        PORTB &= ~(1 << PB0);
        _delay_ms(500);
    }
    
    return 0;  // 理论上不会执行
}

解释

  • DDRB |= (1 << PB0):使用位操作设置PB0为输出。PB0是宏定义(0)。
  • PORTB |= (1 << PB0):置高PB0,电流从VCC流向LED到地。
  • _delay_ms(500):阻塞延时,精确到时钟周期(假设1MHz默认时钟)。
  • 编译命令(AVR-GCC):avr-gcc -mmcu=atmega328p -Os -o main.elf main.c(-Os优化大小)。
  • 生成HEX:avr-objcopy -O ihex main.elf main.hex
  • 烧录后,LED将每秒闪烁一次。

常见问题:如果LED不亮,检查接线(正负极)、电阻值,或时钟配置(默认内部8MHz)。

3.3 时钟配置

AVR有内部RC振荡器(8MHz默认)或外部晶体(16MHz)。配置时钟影响延时和定时器精度。

代码示例(使用外部晶体,16MHz):

#include <avr/io.h>

void init_clock(void) {
    // 对于ATmega328P,熔丝位需设置为外部晶体,这里代码中无法直接改熔丝
    // 但可配置PLL如果需要倍频
    // 实际中,用Arduino IDE自动处理
}

提示:初学者用Arduino的16MHz外部晶体,避免熔丝位烧录错误(可能导致芯片锁死)。

第四部分:核心外设编程技巧

4.1 定时器:精确计时和PWM

AVR有多个定时器。Timer0是8位,Timer1是16位。用于生成PWM(脉宽调制)控制电机或LED亮度。

技巧:使用Timer1生成50Hz PWM(舵机控制)

舵机需要20ms周期,1-2ms高电平脉冲。

#include <avr/io.h>
#include <avr/interrupt.h>  // 中断支持

void init_timer1(void) {
    // 配置为快速PWM模式,非反转
    TCCR1A = (1 << WGM11) | (1 << COM1A1);  // COM1A1: OC1A (PB1) 输出
    TCCR1B = (1 << WGM12) | (1 << WGM13) | (1 << CS11);  // 8分频,16MHz/8=2MHz
    ICR1 = 39999;  // 周期 = (2MHz / (ICR1+1)) = 50Hz
    DDRB |= (1 << PB1);  // PB1 (Arduino D9) 输出
}

void set_servo_angle(uint16_t angle) {
    // 角度0-180映射到1-2ms脉冲 (1000-2000 ticks)
    uint16_t pulse = 1000 + (angle * 1000 / 180);
    OCR1A = pulse;  // 设置比较值
}

int main(void) {
    init_timer1();
    while (1) {
        set_servo_angle(0);  // 0度
        _delay_ms(1000);
        set_servo_angle(90);  // 90度
        _delay_ms(1000);
    }
}

解释

  • TCCR1A/B:配置模式和时钟源。
  • ICR1:定义PWM周期。
  • OCR1A:控制占空比。
  • 这允许精确控制舵机,而无需CPU干预。

4.2 ADC:读取模拟传感器

AVR ADC是10位,参考电压可选(内部1.1V或VCC)。

实战:读取电位器值,控制LED亮度(PWM)

#include <avr/io.h>
#include <util/delay.h>

void init_adc(void) {
    ADMUX = (1 << REFS0);  // 参考电压VCC,通道0 (ADC0)
    ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);  // 使能ADC,128分频
}

uint16_t read_adc(uint8_t channel) {
    ADMUX = (ADMUX & 0xF0) | (channel & 0x0F);  // 选择通道
    ADCSRA |= (1 << ADSC);  // 开始转换
    while (ADCSRA & (1 << ADSC));  // 等待完成
    return ADC;  // 返回10位值 (0-1023)
}

int main(void) {
    init_adc();
    DDRB |= (1 << PB1);  // PB1 PWM输出
    TCCR1A = (1 << COM1A1) | (1 << WGM10);  // 快速PWM 8位
    TCCR1B = (1 << WGM12) | (1 << CS11);  // 8分频
    
    while (1) {
        uint16_t val = read_adc(0);  // 读ADC0
        OCR1A = val / 4;  // 映射到0-255 PWM值
        _delay_ms(10);
    }
}

解释

  • ADC转换需等待ADSC位清零。
  • 值除以4将10位映射到8位PWM。
  • 这实现了模拟输入到数字输出的转换,适用于温度传感器或光敏电阻。

4.3 中断:实时响应

中断允许事件触发代码执行,而非轮询。外部中断(INT0/INT1)用于按钮。

示例:按钮中断控制LED

#include <avr/io.h>
#include <avr/interrupt.h>

volatile uint8_t led_state = 0;  // 全局变量,volatile防止优化

ISR(INT0_vect) {  // INT0中断服务程序 (PD2)
    led_state ^= 1;  // 切换状态
}

int main(void) {
    DDRB |= (1 << PB0);  // LED输出
    EICRA = (1 << ISC01) | (1 << ISC00);  // 上升沿触发
    EIMSK = (1 << INT0);  // 使能INT0
    sei();  // 全局使能中断
    
    while (1) {
        if (led_state) {
            PORTB |= (1 << PB0);
        } else {
            PORTB &= ~(1 << PB0);
        }
    }
}

解释

  • ISR:中断向量,按钮按下时触发。
  • sei():开启中断,必须在main中调用。
  • 这避免了忙等待,提高效率。

4.4 通信:UART串口调试

UART用于与PC通信,输出调试信息。

示例:发送”Hello”到串口(9600波特率,16MHz)

#include <avr/io.h>

void uart_init(void) {
    UBRR0 = 103;  // 9600 baud @ 16MHz
    UCSR0B = (1 << TXEN0);  // 使能发送
}

void uart_transmit(char data) {
    while (!(UCSR0A & (1 << UDRE0)));  // 等待缓冲区空
    UDR0 = data;
}

void uart_print(const char* str) {
    while (*str) {
        uart_transmit(*str++);
    }
}

int main(void) {
    uart_init();
    uart_print("Hello, AVR!\r\n");
    while (1);
}

解释

  • UBRR0:波特率寄存器,计算公式:UBRR = (F_CPU / (16 * BAUD)) - 1。
  • 用串口监视器(如PuTTY)查看输出,帮助调试。

第五部分:高级技巧与优化

5.1 低功耗编程

AVR擅长低功耗。使用睡眠模式减少能耗。

#include <avr/sleep.h>

void enter_sleep(void) {
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    sleep_enable();
    sleep_cpu();  // 进入睡眠
}

在主循环中调用,适合电池项目。

5.2 内存优化

  • 使用PROGMEM存储常量到Flash:const char str[] PROGMEM = "Data";
  • 避免浮点运算,用整数代替(AVR无FPU)。

5.3 调试技巧

  • 用LED或串口输出变量值。
  • 逻辑分析仪(如Saleae)捕获信号。
  • 常见错误:忘记sei()(中断不触发)、寄存器位操作错误(用位掩码)。

第六部分:实战项目:温度监控系统

项目概述

使用ATmega328P、LM35温度传感器(模拟输出)、LCD显示(I2C接口)和蜂鸣器(超温报警)。

硬件连接

  • LM35:VCC、GND、OUT到ADC0。
  • LCD:SDA (PC4)、SCL (PC5)。
  • 蜂鸣器:PB2,通过晶体管驱动。

完整代码(使用I2C LCD库,需安装LiquidCrystal_I2C)

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <Wire.h>  // Arduino Wire库,或用原生I2C代码
#include <LiquidCrystal_I2C.h>  // LCD库

// 初始化LCD (地址0x27,16x2)
LiquidCrystal_I2C lcd(0x27, 16, 2);

void init_adc(void) {
    ADMUX = (1 << REFS0);
    ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
}

uint16_t read_adc(uint8_t channel) {
    ADMUX = (ADMUX & 0xF0) | (channel & 0x0F);
    ADCSRA |= (1 << ADSC);
    while (ADCSRA & (1 << ADSC));
    return ADC;
}

int main(void) {
    // Arduino风格,实际用AVR-GCC需替换Wire/LiquidCrystal为原生代码
    // 为简洁,这里用伪代码表示原生I2C(完整实现需~200行)
    // 实际:实现TWI初始化、起始、发送地址、数据、停止
    
    init_adc();
    DDRB |= (1 << PB2);  // 蜂鸣器
    
    lcd.init();
    lcd.backlight();
    lcd.setCursor(0,0);
    lcd.print("Temp Monitor");
    
    while (1) {
        uint16_t adc_val = read_adc(0);
        // LM35: 10mV/°C, VCC=5V, ADC=1023*5V/1024=5mV/step
        // Temp = (adc_val * 5.0 / 1024.0) * 100; 但用整数避免浮点
        uint16_t temp = (adc_val * 500) / 1024;  // *100 to avoid float, then /10 for decimal
        
        lcd.setCursor(0,1);
        lcd.print("Temp: ");
        lcd.print(temp / 10);
        lcd.print(".");
        lcd.print(temp % 10);
        lcd.print("C ");
        
        if (temp > 300) {  // 30.0°C
            PORTB |= (1 << PB2);  // 蜂鸣器响
            _delay_ms(100);
            PORTB &= ~(1 << PB2);
        }
        
        _delay_ms(1000);
    }
}

解释与扩展

  • ADC部分:如前所述,计算温度需校准(LM35输出10mV/°C,ADC步进~4.88mV)。
  • I2C部分:原生AVR I2C需操作TWCR、TWDR寄存器。示例简化,建议用Arduino测试后移植。
    • 原生I2C起始条件:TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN); while (!(TWCR & (1 << TWINT)));
  • 优化:添加中断定时器,每秒读取一次,避免阻塞。
  • 测试:上传到Arduino,连接传感器,观察LCD显示。超温时蜂鸣器报警。
  • 扩展:添加EEPROM存储历史温度,或WiFi模块(ESP8266)发送数据。

这个项目整合了GPIO、ADC、定时器和通信,是入门后的理想实战。

第七部分:常见问题与故障排除

  • 编译错误:检查头文件路径,确保AVR-Libc安装。
  • 烧录失败:检查编程器连接、熔丝位(勿改CKDIV8)。
  • 代码不工作:用示波器检查引脚电平,或添加调试打印。
  • 资源:AVR Freaks论坛、Microchip文档、GitHub AVR示例。

结论:下一步学习路径

通过本指南,你已掌握AVR基础:从硬件到编程,再到实战项目。继续实践:尝试中断驱动的按键、SPI通信的OLED显示,或FreeRTOS移植。推荐阅读《AVR微控制器编程》和Microchip官网教程。坚持动手,你会快速成为AVR专家!如果有具体问题,欢迎提供细节进一步讨论。