第五章 LED闪烁以及流水灯实验
P2^0表示P2口第0引脚是因为:Keil C51的特殊语法:在这里不是异或运算历史约定:从Intel 8051时代延续下来的语法编译器支持:Keil编译器识别并翻译这种语法地址映射:对应P2口(0xA0)的第0位这是一种编译器级别的语法糖,让程序员能够以更直观的方式操作单个引脚。
目录
讲 的 有 点 啰 嗦 , 建 议 看 着 目 录 看
讲 的 有 点 啰 嗦 , 建 议 看 着 目 录 看
1. 硬件设计
点亮LED灯,需要把接通的引脚设置为低电平。
2. 软件设计
2.1 LED闪烁实验
先看效果

2.1.1 为了使LED闪烁,需要用到延时函数。
// 可调毫秒延时函数(12MHz晶振)
void delay_ms(unsigned int ms) {
unsigned int i, j;
for(i = ms; i > 0; i--)
for(j = 110; j > 0; j--);
}
计算原理(12MHz晶振):
1. 基本时间单位
晶振频率:12MHz = 12,000,000 Hz
机器周期:STC89系列1个机器周期 = 12个时钟周期
1个机器周期时间:
12 / 12,000,000 = 1μs2. 单条指令执行时间
在C语言中,
j--和j > 0这样的语句会被编译成多条汇编指令,大致需要:
j--:约2-4个机器周期
j > 0:约2-4个机器周期循环跳转:约2个机器周期
内层循环
for(j = 110; j > 0; j--)一次大约需要8-10个机器周期3. 具体计算过程
内层循环1次 ≈ 10个机器周期 ≈ 10μs 内层循环110次 ≈ 110 × 10μs = 1100μs = 1.1ms 外层循环1次 = 内层循环110次 ≈ 1.1ms 外层循环ms次 ≈ ms × 1.1ms但实际上我们需要的是精确的1ms,所以:
4. 为什么是110而不是100?
通过实际测试和调整:
如果用
j = 100,实际延时可能只有约0.9ms如果用
j = 110,实际延时接近1ms如果用
j = 120,实际延时约1.1ms110是一个经验值,通过实际测量和调试得出的最接近1ms的数值。
2.1.2 整体代码
#include <STC89C5xRC.H> // 头文件
// 定义P2的第一个引脚为接通LED的引脚
sbit LED = P2^0;
// 可调毫秒延时函数(12MHz晶振)
void delay_ms(unsigned int ms) {
unsigned int i, j;
for(i = ms; i > 0; i--)
for(j = 110; j > 0; j--);
}
void main(){
while(1){
LED = 0;
delay_ms(500);
LED = 1;
delay_ms(500);
}
}
2.1.3 Tips:
前面说了延时函数,现在说一下三个问题:
为什么 sbit LED = P2^0; 这一句就可以把LED定义为P2的第一个引脚?
又为什么在江协科技中,P20可以直接用来控制P2的第一个引脚?
为什么我会用sbit LED = P2^0;定义LED而不是像视频那样直接用P20?
1. 语法约定,而非数学运算
P2^0看起来像魔法值,但其实这是C51编译器的一种特殊语法约定。
P2^0中的^不是异或运算,而是C51编译器的特殊语法!
在标准C语言中:^ 表示按位异或运算
在Keil C51中:
sfr_name^bit_position表示特殊功能寄存器的特定位这是Keil公司为51单片机专门扩展的语法。
2. 底层原理:特殊功能寄存器(SFR)
51单片机的内存映射:
P0口地址:0x80 P1口地址:0x90 P2口地址:0xA0 // 这就是P2 P3口地址:0xB0头文件中的定义:
在STC89C5xRC.H中,
sfr P2 = 0xA0; // 将P2关联到地址0xA0 sbit P20 = P2^0; // P2的第一个引脚在头文件中已经被定义,可以直接使用3. 编译器如何处理
P2^0当编译器看到:
sbit LED = P2^0;它会:
找到P2的基地址:
0xA0计算第0位的地址:
0xA0 × 8 + 0 = 0xA0的第0位生成对应的位操作指令
下面是 2 的解释:(有点长,但只要看得下去,有点编程思维,基本都能看得懂,加油~)
0xA0 × 8 + 0 = 0xA0的第0位 为什么会可以这么算? 这是由51单片机特殊的存储器结构决定的 1. 51单片机的存储器结构 51单片机有两个不同的地址空间: 字节地址空间:用于访问整个字节(8位) 位地址空间:用于访问单个位(1位) 2. 位地址的计算原理 ·核心概念: ·在51单片机中,只有地址为0x80-0xFF的特殊功能寄存器(SFR)才能进行位寻址。 ·在头文件中,字段类型为sfr(sfr P2=0xA0;)的就是,可以进行定位寻址。 ·而在头文件中,定义的sbit P20=P2^0;已经不能再进行定位寻址。 ·为什么? ·sfr P2 = 0xA0; 的含义: ·sfr P2 = 0xA0; // 定义P2为特殊功能寄存器,地址为0xA0 ·sfr:告诉编译器这是一个特殊功能寄存器 ·0xA0:这个寄存器的字节地址 ·可以位寻址:因为0xA0在0x80-0xFF范围内 ·sbit P20 = P2^0; 的含义: ·sbit P20 = P2^0; // 定义P20为P2寄存器的第0位 ·sbit:告诉编译器这是一个位变量 ·P2^0:指向P2寄存器的第0位 ·不可以再位寻址:因为sbit已经是最小的位单位了 ·层次关系理解: P2寄存器 (sfr, 地址0xA0) / | | | | | | \ / | | | | | | \ / | | | | | | \ P2^0 P2^1 ... P2^7 (sbit, 位地址0xA0-0xA7) ·sfr:字节级别操作(8位一起操作) ·sbit:位级别操作(单独1位操作) ·操作方式的区别: ·对sfr P2的操作(字节操作): ·P2 = 0xFE; // 1111 1110 → 操作整个P2口 ·P2 = P2 & 0x7F; // 位运算操作整个字节 ·对sbit P20的操作(位操作): ·P20 = 0; // 只操作P2.0这一位,其他位不变 ·P20 = 1; // 只操作P2.0这一位 ·if (P20 == 0) // 只读取P2.0这一位的状态 ·所以为什么sbit不能再位寻址? ·因为sbit已经是最小的可寻址单位了!就像: ·一栋楼(sfrP2)有8个房间(sbitP20-P27) ·你可以管理整栋楼(操作P2) ·也可以管理单个房间(操作P20) ·但不能再管理"房间的门把手"了——因为房间已经是最小单位 ·实际编译结果: ·当编译器看到: ·sbit P20 = P2^0; ·P20 = 1; ·它会生成类似这样的汇编代码: ·SETB 0xA0 ; 将位地址0xA0设为1 ·而看到: ·P2 = 0xFF; ·它会生成: MOV 0xA0, #0xFF ; 向地址0xA0写入0xFF ·总结: ·特性 sfr P2 = 0xA0; sbit P20 = P2^0; ·类型 特殊功能寄存器 位变量 ·操作单位 8位(字节) 1位(比特) ·可寻址性 可字节寻址,也可位寻址 只能位操作,不能再分解 ·地址 字节地址0xA0 位地址0xA0 ·sfr P2 = 0xA0; 定义了一个可位寻址的寄存器 ·sbit P20 = P2^0; 定义了一个位变量,不能再位寻址 ·只有地址在0x80-0xFF的sfr才能进行位寻址4. 位的实际地址计算
51单片机支持位寻址,位地址范围:
0x00~0x7F
端口 基地址 位0地址 位1地址 位2地址 位3地址 位4地址 位5地址 位6地址 位7地址 P0 0x80 0x80 0x81 0x82 0x83 0x84 0x85 0x86 0x87 P1 0x90 0x90 0x91 0x92 0x93 0x94 0x95 0x96 0x97 P2 0xA0 0xA0 0xA1 0xA2 0xA3 0xA4 0xA5 0xA6 0xA7 P3 0xB0 0xB0 0xB1 0xB2 0xB3 0xB4 0xB5 0xB6 0xB7 所以
P2^0就对应地址0xA0的第0位。5. 为什么这样设计?
历史原因:
51单片机架构古老(1980年设计)
需要提供位操作功能来提高效率
^符号在当时没有被广泛使用,所以被选为位寻址语法优点:
代码可读性好:
P2^0比0xA0更直观可移植性:换到P3口只需改为
P3^0编译器优化:编译器能生成高效的位操作指令
6.验证方法
直接使用代码测试
#include <STC89C5xRC.H> sbit LED0 = P2^0; sbit LED1 = P2^1; sbit LED2 = P2^2; void main() { LED0 = 0; // 只操作P2.0 LED1 = 1; // 只操作P2.1 LED2 = 0; // 只操作P2.2 while(1); }用万用表测量P2口各引脚电压,会发现只有P2.0和P2.2为低电平,P2.1为高电平。
7. 注意事项:
这种语法只在Keil C51编译器中有效,在其他编译器中:
GCC for 51:可能不支持这种语法
SDCC:使用
__sbit关键字标准C编译器:完全不支持
8.总结:
P2^0表示P2口第0引脚是因为:
Keil C51的特殊语法:^在这里不是异或运算
历史约定:从Intel 8051时代延续下来的语法
编译器支持:Keil编译器识别并翻译这种语法
地址映射:对应P2口(0xA0)的第0位
这是一种编译器级别的语法糖,让程序员能够以更直观的方式操作单个引脚。
2.2 LED流水灯实验
先看效果

所谓流水灯,就是让几个灯逐个点亮、熄灭,这个时候就需要用到 for循环 和 移位操作符,即IO口按顺序输出低电平。主程序如下:
#include <STC89C5xRC.H> // 头文件
// 定义P2的第一个引脚为接通LED的引脚
sbit LED = P2^0;
// 可调毫秒延时函数(12MHz晶振)
void delay_ms(unsigned int ms) {
unsigned int i, j;
for(i = ms; i > 0; i--)
for(j = 110; j > 0; j--);
}
void main()
{
unsigned char i;
while(1)
{
for(i = 0; i < 8 ;i++)
{
// 左移操作 <<; 右移操作 >>
P2 = ~(0x01 << i); // 0000 0001
delay_ms(500);
}
}
}
讲解:
0x01是一个8位的二进制数,其形式是00000001。左移操作<<将这个数按位向左移动,移动的位数由循环变量i决定。- 在循环中,
i的值从 0 递增到 7。因此,左移操作0x01 << i将会得到不同位被置位的结果。例如,当i为 0 时,结果是00000001,当i为 1 时,结果是00000010,以此类推。~运算符是按位取反操作符,它将每个位上的值取反。因此,~(0x01 << i)将会生成一个具有与左移操作相反的位值的二进制数。- 最后,
P2 = ~(0x01 << i);将取反后的值写入到P2寄存器中,控制对应的 LED 灯的亮灭状态。
除此之外,KEIL有可以使用的库函数,实现代码如下:
#include <STC89C5xRC.H> // 头文件
// 可调毫秒延时函数(12MHz晶振)
void delay_ms(unsigned int ms) {
unsigned int i, j;
for(i = ms; i > 0; i--)
for(j = 110; j > 0; j--);
}
void main(){
// 明确设置初始状态:1111 1110
P2 = 0xFE;
// 1000ms -> 1秒闪一次
delay_ms(1000);
while(1)
{
// _crol_ 循环左移函数
P2 = _crol_(P2, 1);
// 1000ms -> 1秒闪一次
delay_ms(1000);
}
}
讲解:
_crol_ 和 _cror_
_crol_- 循环左移 (Circle Rotate Left)
功能:将数据的各位向左循环移动
特点:最高位移出后,会填充到最低位
效果:数据循环移动,不会丢失任何位
_cror_- 循环右移 (Circle Rotate Right)
功能:将数据的各位向右循环移动
特点:最低位移出后,会填充到最高位
效果:数据循环移动,不会丢失任何位
需要包含头文件
intrins.h,以下是头文件内容。需要注意的是,STC89C5xRC.H 包含了
intrins.h 以及 stdio.h
工作原理图示
_crol_循环左移示例(8位数据):初始值: 1111 0000 (0xF0) _crol_(0xF0, 1): 1110 0001 (0xE1) ┌─ 1移出 ┌─ 填充到最低位 1111 0000 → 1110 0001 _crol_(0xF0, 2): 1100 0011 (0xC3) _crol_(0xF0, 3): 1000 0111 (0x87)
_cror_循环右移示例(8位数据):初始值: 1111 0000 (0xF0) _cror_(0xF0, 1): 0111 1000 (0x78) ┌─ 0移出 ┌─ 填充到最高位 1111 0000 → 0111 1000 _cror_(0xF0, 2): 0011 1100 (0x3C) _cror_(0xF0, 3): 0001 1110 (0x1E)与普通移位操作符的区别
操作 代码 结果(以0xF0为例) 特点 普通左移 a << 11110 0000 (0xE0) 最高位丢失,最低位补0 循环左移 _crol_(a, 1)1110 0001 (0xE1) 最高位移到最低位 普通右移 a >> 10111 1000 (0x78) 最低位丢失,最高位补0 循环右移 _cror_(a, 1)0111 1000 (0x78) 最低位移到最高位
更多推荐




所有评论(0)