第一部分:基础与准备
什么是ATmega16?**
ATmega16是由Microchip(原Atmel)公司生产的一款基于 AVR RISC架构 的8位低功耗CMOS单片机,它具有以下特点:

(图片来源网络,侵删)
- 高性能、精简指令集:大多数指令执行时间为单个时钟周期。
- 16KB片上可编程Flash存储器:用于存放你的C程序代码。
- 1KB SRAM:用于程序运行时的变量存储。
- 512KB EEPROM:用于掉电不丢失的数据存储。
- 32个通用I/O引脚:可以灵活配置为输入或输出。
- 丰富的外设:
- 2个8位定时器/计数器
- 2个16位定时器/计数器
- 8通道10位ADC(模数转换器)
- 可编程的串行USART
- 面向字节的两线式串行接口(I²C)
- 串行外设接口
- 可看门狗定时器
为什么用C语言而不是汇编?**
对于初学者和大多数项目,C语言是更好的选择:
- 可读性强:代码更接近自然语言,易于理解和维护。
- 开发效率高:不需要关心寄存器的具体位操作,编译器会帮你处理。
- 可移植性好:修改少量代码即可移植到其他AVR单片机甚至其他架构的MCU。
- 丰富的库函数:有大量现成的库函数可以使用,简化了底层操作。
第二部分:开发环境搭建
在开始写代码之前,你需要准备以下工具:
-
硬件:
- ATmega16单片机(或带有ATmega16的开发板,如AVR Dragon、STK500或自制的最小系统板)。
- USB ISP下载器(如USBasp、AVRISP mkII)。
- 面包板、杜邦线、LED、电阻、按键等基础元器件。
-
软件:
(图片来源网络,侵删)- 代码编辑器/IDE:
- Atmel Studio (推荐):Microchip官方IDE,集成了代码编辑器、AVR-GCC编译器、AVRDUDE下载工具和强大的调试器,功能最完善。
- VS Code + 插件:轻量级,通过安装C/C++和PlatformIO插件,可以实现强大的跨平台开发。
- 编译器:通常是 AVR-GCC,一个专门为AVR优化的GCC版本,Atmel Studio和PlatformIO都会自动集成它。
- 下载/烧录工具:AVRDUDE,一个命令行工具,用于将编译生成的
.hex文件写入单片机,Atmel Studio和PlatformIO会调用它。
- 代码编辑器/IDE:
第三部分:C语言核心要素与AVR特有概念
标准的C语言语法同样适用于AVR,但你需要了解一些与硬件紧密相关的概念。
I/O端口操作
ATmega16有4个8位I/O端口:PORTA, PORTB, PORTC, PORTD,每个端口有8个引脚(PA0-PA7, PB0-PB7等)。
操作I/O端口需要操作三个寄存器:
- DDRx (Data Direction Register x):数据方向寄存器。
DDRx.n = 1:将引脚n设置为输出。DDRx.n = 0:将引脚n设置为输入。
- PORTx (Port x Output Register):端口输出寄存器。
- 对于输出引脚:
PORTx.n = 1输出高电平,PORTx.n = 0输出低电平。 - 对于输入引脚:
PORTx.n = 1启用内部上拉电阻,PORTx.n = 0禁用内部上拉电阻。
- 对于输出引脚:
- PINx (Port x Input Register):端口输入寄存器。
- 用于读取输入引脚的电平状态。
if (PINA & (1 << PA0))表示判断PA0引脚是否为高电平。
- 用于读取输入引脚的电平状态。
示例:让PB0引脚上的LED闪烁

(图片来源网络,侵删)
#include <avr/io.h> // 包含所有ATmega16的特殊功能寄存器定义
// 延时函数(粗略延时,不精确)
void delay_ms(unsigned int ms) {
unsigned int i, j;
for (i = 0; i < ms; i++)
for (j = 0; j < 1000; j++); // 循环次数取决于时钟频率
}
int main(void) {
// 1. 设置PB0为输出
// 方法1: 位操作
// DDRB |= (1 << PB0);
// 方法2: 整数赋值 (更推荐)
DDRB = 0x01; // 将DDRB寄存器的最低位置1,其他位保持不变
while (1) // 无限循环
{
// 2. PB0输出高电平,LED熄灭(如果是共阳极)或亮(如果是共阴极)
PORTB |= (1 << PB0); // 方法1
// PORTB = 0x01; // 方法2
delay_ms(500); // 延时500ms
// 3. PB0输出低电平,LED亮或熄灭
PORTB &= ~(1 << PB0); // 方法1: 清除PB0位
// PORTB = 0x00; // 方法2
delay_ms(500); // 延时500ms
}
}
位操作(Bit Manipulation)
这是嵌入式C的精髓,用于精确控制寄存器的某一位。
- (按位或): 用于置位(设置为1)。
DDRB |= (1 << PB5);// 设置PB5为输出 &=(按位与): 用于清零(设置为0)。PORTB &= ~(1 << PB5);// 清除PB5的输出^=(按位异或): 用于翻转(取反)。PINB ^= (1 << PB5);// 翻转PB5的电平&(按位与): 用于读取。if (PINA & (1 << PA0))// 检查PA0是否为高电平<<(左移):1 << n生成一个只有第n位为1的二进制数。>>(右移): 将二进制数右移。- (按位取反): 将所有0变1,1变0,常用于生成清零掩码,如
~(1 << PB0)。
头文件 <avr/io.h> 和 `<util/delay.h>``
<avr/io.h>:必须包含,它定义了所有与ATmega16硬件相关的寄存器(如DDRB, PORTA, TCCR1A等)和位名称(如PB0, WGM10等)。<util/delay.h>:提供毫秒和微秒级的延时函数。使用前必须在项目设置中指定正确的CPU频率(通常是#define F_CPU 8000000UL,8MHz晶振)。
第四部分:常用外设编程实例
定时器/计数器 (以Timer1为例)
定时器是单片机的核心,用于精确定时、产生PWM波等。
目标:使用Timer1,每1秒翻转一次PB0引脚的电平。
思路:
- 设置Timer1为正常模式。
- 设置一个预分频器,降低时钟频率,以获得更长的计时范围。
- 计算需要设定的计数值,使定时器在1秒后溢出。
- 启用定时器的溢出中断。
- 编写中断服务函数,在中断中翻转PB0的电平。
代码实现:
#include <avr/io.h>
#include <avr/interrupt.h>
#define F_CPU 8000000UL // 假设使用8MHz晶振
volatile uint8_t timer_overflow_count = 0;
// Timer1 overflow interrupt service routine
ISR(TIMER1_OVF_vect) {
timer_overflow_count++;
// 每8次溢出,总计时间为 8 * 0.0256s = 0.2048s (接近1/5秒)
// 为了得到1秒,需要调整分频和计算,这里简化逻辑
if (timer_overflow_count >= 78) { // 8MHz / 1024 / 256 = ~30.5次/秒,取31次约为1秒
PORTB ^= (1 << PB0); // 翻转PB0
timer_overflow_count = 0;
}
}
int main(void) {
// 设置PB0为输出
DDRB = (1 << PB0);
// 设置定时器1
TCCR1A = 0x00; // 正常模式,OC1A/OC1B disconnected
TCCR1B = (1 << CS12) | (1 << CS10); // 设置预分频器为1024
// CS12=1, CS11=0, CS10=1 -> 1024分频
// 8MHz / 1024 = 7812.5 Hz, 每次计数时间为 1/7812.5s ≈ 128μs
// 16位定时器最大计数值65536,溢出时间 = 65536 * 128μs ≈ 8.388s
// 启用定时器1溢出中断
TIMSK |= (1 << TOIE1);
// 全局中断使能
sei();
while (1) {
// 主循环可以执行其他任务,定时由中断处理
// 注意:全局变量 timer_overflow_count 需要是 volatile
}
}
ADC(模数转换器)
目标:读取PA0引脚上电位器的电压值,并通过串口打印到电脑上。
思路:
- 设置PA0为输入。
- 设置ADC的参考电压(如AVCC)。
- 设置ADC的通道(选择ADC0,对应PA0)。
- 设置ADC预分频器,为ADC提供合适的时钟(50kHz - 200kHz)。
- 启动ADC转换,并等待转换完成。
- 读取ADC结果。
- (可选)配置USART,将结果发送到电脑。
代码实现:
#include <avr/io.h>
#include <util/delay.h>
void ADC_Init() {
// ADMUX: 参考电压为AVCC, ADC0通道, 右对齐
ADMUX = (1 << REFS0); // AVCC with external capacitor at AREF pin
// ADCSRA: ADC使能, 预分频为128 (8MHz / 128 = 62.5kHz)
ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
}
uint16_t ADC_Read(uint8_t channel) {
// 选择通道 (0-7)
ADMUX = (ADMUX & 0xF0) | (channel & 0x0F);
// 启动转换
ADCSRA |= (1 << ADSC);
// 等待转换完成
while (ADCSRA & (1 << ADSC));
// 返回10位结果 (ADCH和ADCL)
return ADC;
}
int main(void) {
// 初始化ADC
ADC_Init();
// 设置ADC结果为输出 (用于调试,可以用LED亮度表示)
DDRB = 0xFF; // PB0-PB7全部为输出
uint16_t adc_value;
while (1)
{
adc_value = ADC_Read(0); // 读取通道0 (PA0)
// 将10位结果映射到8位 (0-255) 并输出到PORTB
// 方法1: 简单移位
// PORTB = (adc_value >> 2);
// 方法2: 更精确的缩放
PORTB = (adc_value / 4);
_delay_ms(100);
}
}
第五部分:项目实践
项目1:呼吸灯
目标:实现LED亮度由暗到亮,再由亮到暗的循环变化。
原理:利用PWM(脉冲宽度调制),通过快速开关LED,并改变高电平所占的时间比例(占空比),人眼就会感觉到亮度的变化。
实现:
- 选择一个支持PWM的定时器(如Timer1)。
- 设置定时器为快速PWM模式。
- 设置预分频器,获得合适的PWM频率(> 1kHz,避免人眼看到闪烁)。
- 通过改变OCR1A(输出比较寄存器A)的值,来改变PWM的占空比,从而控制LED亮度。
项目2:温湿度监测站
目标:读取DHT11/DHT22温湿度传感器的数据,并在LCD1602液晶屏上显示。
原理:
- DHT11:单总线协议通信,MCU发送开始信号,传感器响应后送出40位数据(湿度整数、湿度小数、温度整数、温度小数、校验和)。
- LCD1602:并行或串行接口,需要初始化LCD,设置显示模式、光标位置,然后发送要显示的字符ASCII码。
实现:
- 编写DHT11的时序驱动函数(
start_signal(),read_bit(),read_byte())。 - 编写LCD1602的底层驱动函数(
send_command(),send_data())。 - 在
main函数中,循环读取DHT11数据,格式化后通过LCD1602显示出来。
总结与建议
- 从点亮一个LED开始:这是嵌入式开发的 "Hello, World!",能让你快速建立信心。
- 善用数据手册:遇到任何关于寄存器、引脚、时序的问题,数据手册是你的第一且最重要的参考资料。
- 先模仿,再创造:多看、多分析别人的代码,理解其工作原理,然后尝试修改和扩展。
- 学会调试:善用LED、串口打印(
printf重定向)等手段来观察程序运行状态。 - 理解底层:虽然C语言封装了细节,但了解寄存器的工作原理会让你在遇到复杂问题时游刃有余。
希望这份指南能帮助你顺利入门ATmega16的C语言程序设计!祝你学习愉快!
