第二周:单片机基础应用笔记
第一讲:流水灯与IO控制
点亮LED——初识GPIO
整体控制(群控模式)
可以把P1口看作一个有8个开关的总控板,一次性设置所有开关状态。
#include <REGX52.H>
void main()
{
while(1)
{
P1 = 0xAA; // 二进制 1010 1010,间隔点亮LED
}
}
比喻:像电闸总开关,一推全亮/全灭。
单独控制(精细操作)
每个IO口就像独立的电灯开关,可单独操作。
#include <REGX52.H>
void main()
{
while(1)
{
P1_0 = 0; // 第一个灯亮(低电平驱动)
P1_1 = 1; // 第二个灯灭
P1_2 = 1; // 第三个灯灭
P1_3 = 0; // 第四个灯亮
}
}
LED闪烁——引入时间概念
延时函数的生成
使用STC-ISP工具生成精确延时,就像设置一个“定时沙漏”。
操作步骤:
- 打开STC-ISP → 软件延时计算器
- 选择:12MHz, STC-Y1
- 生成代码
void Delay(unsigned int xms) // @12.000MHz
{
while(xms--)
{
unsigned char i, j;
i = 2;
j = 239;
do {
while (--j);
} while (--i);
}
}
实现流水闪烁
让灯光像接力跑一样逐个传递。
void main()
{
while(1)
{
P1_0 = 0; P1_3 = 1;
Delay(1000); // 亮1秒
P1_1 = 0; P1_0 = 1;
Delay(1000);
P1_2 = 0; P1_1 = 1;
Delay(1000);
P1_3 = 0; P1_2 = 1;
Delay(1000);
}
}
使用库函数实现流水灯
调用现成的“灯光特效库”,让代码更简洁。
#include <REGX52.H>
#include <intrins.h> // 内含循环移位函数
// 延时函数(同上)
void Delay(unsigned int xms){...}
unsigned char ucLed = 0xFE; // 初始值:1111 1110(仅第一个灯亮)
void main()
{
while(1)
{
ucLed = _crol_(ucLed, 1); // 循环左移1位
P1 = ucLed; // 输出到LED
Delay(500); // 延时0.5秒
}
}
函数说明:
_crol_(P1, n):P1向左循环移n位_cror_(P1, n):P1向右循环移n位
第二讲:按键交互
按键原理
按键就像一个“电流阀门”,按下时接通电路,改变引脚电平。
连接方式决定电平:
- 左边接地(GND) → 按下时右边引脚=0(低电平)
- 左边接VCC → 按下时右边引脚=1(高电平)
独立按键引脚对应:
- S1 → P3_4
- S2 → P3_5
- S3 → P3_6
- S4 → P3_7
按键读取函数
给每个按键分配一个“身份证号”。
unsigned char Key_Read()
{
unsigned char temp = 0;
if(P3_4 == 0) temp = 1; // S1按下→返回1
if(P3_5 == 0) temp = 2; // S2按下→返回2
if(P3_6 == 0) temp = 3; // S3按下→返回3
if(P3_7 == 0) temp = 4; // S4按下→返回4
return temp;
}
按键检测“四行金句”
这是检测按键动作的核心逻辑,必须熟记!
unsigned char Key_Val, Key_Down, Key_Up, Key_Old;
Key_Val = Key_Read(); // 1. 读取当前按键状态
Key_Down = Key_Val & (Key_Val ^ Key_Old);// 2. 检测按下瞬间(下降沿)
Key_Up = ~Key_Val & (Key_Val ^ Key_Old); // 3. 检测释放瞬间(上升沿)
Key_Old = Key_Val; // 4. 保存当前状态供下次比较
比喻:
Key_Down:发现有人刚坐下(按下瞬间)Key_Up:发现有人刚站起来(释放瞬间)Key_Old:记住上一刻谁在坐着
按键控制LED
void main()
{
while(1)
{
// 四行金句检测按键
Key_Val = Key_Read();
Key_Down = Key_Val & (Key_Val ^ Key_Old);
Key_Up = ~Key_Val & (Key_Val ^ Key_Old);
Key_Old = Key_Val;
// 按键功能映射
if(Key_Down == 1) P1_0 = 0; // S1按下→LED1亮
if(Key_Up == 2) P1_0 = 1; // S2释放→LED1灭
if(Key_Old == 3) P1_1 = 0; // S3按住期间→LED2亮
else P1_1 = 1; // 否则→LED2灭
}
}
按键控制流水灯启停
引入“系统标志位”作为总开关。
unsigned char ucLed = 0xFE;
unsigned char Key_Val, Key_Down, Key_Up, Key_Old;
bit System_Flag = 0; // 0:停止,1:运行
void main()
{
while(1)
{
// 按键检测(略)
if(System_Flag == 1) // 系统运行中
{
ucLed = _crol_(ucLed, 1);
P1 = ucLed;
Delay(500);
}
// 按键控制标志位
if(Key_Down == 1) System_Flag = 1; // S1启动
if(Key_Down == 2) System_Flag = 0; // S2停止
}
}
矩阵键盘
把16个按键排列成4×4矩阵,节省引脚资源。
unsigned char Key_Read()
{
unsigned char temp = 0;
// 扫描第一行
P3_0 = 0; P3_1 = 1; P3_2 = 1; P3_3 = 1;
if(P3_4 == 0) temp = 1;
if(P3_5 == 0) temp = 2;
if(P3_6 == 0) temp = 3;
if(P3_7 == 0) temp = 4;
// 扫描第二行(同理)
P3_0 = 1; P3_1 = 0; P3_2 = 1; P3_3 = 1;
if(P3_4 == 0) temp = 5;
// ... 以此类推
return temp;
}
注意:使用矩阵键盘时,独立按键引脚被占用,二者不能同时使用。
综合项目:彩灯控制系统
实现多种灯光模式的可控切换。
功能需求:
- 模式1:顺序流水(左→右)
- 模式2:逆序流水(右→左)
- 模式3:对称展开(从中间向两边)
- 模式4:对称收缩(从两边向中间)
按键定义:
- S1:启动
- S2:暂停
- S3:模式+1
- S4:模式-1
// 关键代码段
unsigned char LED_Mode = 0; // 当前模式
unsigned char LED_Date[4] = {0x7E, 0xBD, 0xDB, 0xE7}; // 对称模式数据
bit System_Flag = 1; // 默认启动
void main()
{
while(1)
{
// 按键检测(略)
// 按键处理
switch(Key_Down)
{
case 1: System_Flag = 1; break; // 启动
case 2: System_Flag = 0; break; // 暂停
case 3: LED_Mode++; if(LED_Mode == 4) LED_Mode = 0; break;
case 4: LED_Mode--; if(LED_Mode == 255) LED_Mode = 3; break;
}
// 灯光控制
if(System_Flag == 1)
{
switch(LED_Mode)
{
case 0: /* 模式1代码 */ break;
case 1: /* 模式2代码 */ break;
case 2: /* 模式3代码 */ break;
case 3: /* 模式4代码 */ break;
}
}
}
}
第三讲:数码管显示
数码管结构
数码管就像“数字积木”,由8个LED段(a-g+dp)组成,可拼出0-9的数字。
硬件特性:
- 6位数码管:位置编号0-5(从左到右)
- 每位可独立显示不同数字
静态显示(单数字)
只能在一个固定位置显示一个数字。
// 位选数组:选择哪一位数码管
unsigned char Seg_Wela[] = {0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF};
// 段选数组:显示什么数字(0-9),最后0x00表示全灭
unsigned char Seg_Dula[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66,
0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x00};
// 显示函数:三步法(消影→位选→段选)
void Seg_Disp(unsigned char wela, dula)
{
P0 = 0x00; // 1. 消影(清除残影)
P2_6 = 1; P2_6 = 0; // 锁存消影数据
P0 = Seg_Wela[wela]; // 2. 选择显示位置
P2_7 = 1; P2_7 = 0; // 锁存位选数据
P0 = Seg_Dula[dula]; // 3. 显示具体数字
P2_6 = 1; P2_6 = 0; // 锁存段选数据
}
动态显示(多数字)
利用人眼视觉暂留,快速切换显示不同位置的数字。
核心思想
比喻:就像快速转动的手电筒,虽然每次只照一个地方,但转得够快时,看起来所有地方都是亮的。
#include <REGX52.H>
// 数码管缓冲区:存储6位数码管要显示的数字
unsigned char Seg_Buf[] = {1, 2, 3, 4, 5, 6};
// 定时器初始化(1ms中断一次)
void Timer0Init(void)
{
TMOD &= 0xF0; TMOD |= 0x01; // 定时器0,模式1
TL0 = 0x18; TH0 = 0xFC; // 1ms定时初值
TF0 = 0; // 清除标志
TR0 = 1; // 启动定时器
ET0 = 1; // 允许定时器中断
EA = 1; // 开启总中断
}
// 定时器中断服务函数
void Timer0Service() interrupt 1
{
static unsigned char Seg_Pos = 0; // 当前显示位置
TL0 = 0x18; TH0 = 0xFC; // 重装初值
// 切换到下一位
if(++Seg_Pos == 6) Seg_Pos = 0;
// 显示该位置的数字
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos]);
}
void main()
{
Timer0Init(); // 初始化定时器
while(1)
{
// 主循环可更新Seg_Buf内容
// 例如:显示变量Time的百位、十位、个位
// Seg_Buf[0] = Time / 100 % 10;
// Seg_Buf[1] = Time / 10 % 10;
// Seg_Buf[2] = Time % 10;
}
}
数字分解技巧
如何把一个整数分解为各个数位?
unsigned int Time = 567; // 要显示的数字
// 分解方法(通用公式):
Seg_Buf[0] = Time / 100 % 10; // 百位:567÷100=5
Seg_Buf[1] = Time / 10 % 10; // 十位:567÷10=56 → 56%10=6
Seg_Buf[2] = Time % 10; // 个位:567%10=7
关键知识点总结
| 概念 | 比喻 | 核心要点 |
|---|---|---|
| GPIO控制 | 电灯开关 | P1整体赋值 vs P1_x单独控制 |
| 延时函数 | 定时沙漏 | 用STC-ISP生成,控制时间节奏 |
| 循环移位 | 旋转木马 | _crol_()左移,_cror_()右移 |
| 按键检测 | 门铃感应 | “四行金句”检测按下/释放瞬间 |
| 矩阵键盘 | 交叉路口 | 行列扫描,节省引脚 |
| 数码管 | 数字积木 | 位选(哪里)+段选(什么) |
| 动态显示 | 手电筒快转 | 利用视觉暂留,定时器刷新 |
| 中断系统 | 闹钟提醒 | 定时到点自动执行,不占用主循环 |
编程思想提升
- 状态机思维:用
System_Flag控制系统状态 - 模块化设计:功能分离(按键、显示、灯光独立)
- 缓冲区概念:
Seg_Buf作为显示数据中间站 - 中断服务:时间敏感操作交给中断处理
- 位操作技巧:灵活运用
&、|、^、~进行位运算
学习如同点亮LED,需要耐心连接每个引脚,当所有知识正确连通时,智慧的灯光自然就会亮起。 ![]()