Keil5 C51和Keil5 MDK的区别
两者都是Keil系列软件,但前者是用来开发51单片机的,后者是用来开发ARM系列,比如STM32的。
双击Keil5软件进入,我们需要编写代码让单片机去识别,在写代码之前我们需要新建一个项目,相当于提供一个办公桌(类似于Visual studio)
最好是在桌面创建文件夹后,再创建项目子文件夹并命名位Project
虽然没有STC89C52,我们可以选择AT89C52代替
一般不需要更改启动文件,选择否
添加c语言文件
命名与建立
生成hex文件,然后在stc-isp中去下载到单片机中
**先选择单片机型号,**我购买的单片机型号是STC89C52RC,**点击打开程序文件,**然后找到2-1文件夹中的object文件夹找到.hex文件,选择打开,**然后点击下载/编程,**再开关单片机即可
和2-1一样的方式,通过新建文件夹,然后新建工程,找到单片机型号,然后创建.c文件,写入以下代码
#include
void main()
{
while(1)
{
P2=0xFE;
P2=0xFF;
}
}
但此时可以发现LED不是那么的亮,这是因为单片机执行代码速度特别快,看不出来在闪,我们要让亮和灭以后延时500ms,才能看出来,可以通过stc-isp生成
生成的代码如下:(还需要添加头文件)
void Delay500ms() //@12.000MHz
{
unsigned char i, j, k;
_nop_();
i = 4;
j = 205;
k = 187;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
每次都需要生成.hex文件,别忘了!
最后的代码:
#include
#include
void Delay500ms() //@12.000MHz
{
unsigned char i, j, k;
_nop_();
i = 4;
j = 205;
k = 187;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void main()
{
while(1)
{
P2=0xFE;
Delay500ms();
P2=0xFF;
Delay500ms();
}
}
通过顺序执行代码,逐个控制亮灭,可以实现流水灯,将LED闪烁的代码加以复制粘贴再修改即可
#include
#include
void Delay500ms() //@12.000MHz
{
unsigned char i, j, k;
_nop_();
i = 4;
j = 205;
k = 187;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
int main()
{
while(1)
{
P2=0xFE; //1111 1110
Delay500ms();
P2=0xFD; //1111 1101
Delay500ms();
P2=0xFB; //1111 1011
Delay500ms();
P2=0xF7; //1111 0111
Delay500ms();
P2=0xEF; //1110 1111
Delay500ms();
P2=0xDF; //1101 1111
Delay500ms();
P2=0xBF; //1011 1111
Delay500ms();
P2=0x7F; //0111 1111
Delay500ms();
}
}
现在是延迟500ms,那么我们如果想要200ms延迟呢?可以通过对Delay函数入手,通过给参数实现
void Delay1ms(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
xms需要指定一个数据类型(存数据的小盒子,存起来才能算),integer(整型)–int,16位(单片机)
#include
#include
void Delay1ms(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms) //非0为真,先执行一次即为延时1ms,然后自减到0
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
int main()
{
while(1)
{
P2=0xFE; //1111 1110
Delay1ms(100);
P2=0xFD; //1111 1101
Delay1ms(100);
P2=0xFB; //1111 1011
Delay1ms(100);
P2=0xF7; //1111 0111
Delay1ms(100);
P2=0xEF; //1110 1111
Delay1ms(100);
P2=0xDF; //1101 1111
Delay1ms(100);
P2=0xBF; //1011 1111
Delay1ms(100);
P2=0x7F; //0111 1111
Delay1ms(100);
}
}
单片机通电时所有IO口都是高电平
寄存器写一个值会送到IO口上,同样会检测IO口的电平读回来
按键松开,读寄存器,值为1;按下时读寄存器为0
P2=0xFE
实际上是通过控制寄存器实现的,寄存器8个为一组;如果直接操作P2,则需要同时给8个赋值,现在只想操作最低位的LED,有什么办法能实现呢?
打开头文件,找到对位寄存器的声明
/*------------------------------------------------
P2 Bit Registers
------------------------------------------------*/
sbit P2_0 = 0xA0;
sbit P2_1 = 0xA1;
sbit P2_2 = 0xA2;
sbit P2_3 = 0xA3;
sbit P2_4 = 0xA4;
sbit P2_5 = 0xA5;
sbit P2_6 = 0xA6;
sbit P2_7 = 0xA7;
有了位声明,可以直接用
现在就是要实现按下实现P2_0=0
,松开实现P2_0=1
#include
int main()
{
while(1)
{
if(P3_1==0) //K1接在了P31,直接读这个寄存器
{
P2_0=0;
}
else
{
P2_0=1;
}
}
}
for循环可以用来产生固定循环次数
首先了解一下按键的抖动
肉眼可能无法观察,比如日常生活中的开关灯,但单片机执行速率是MHZ级别的,会有明显影响,如何消除这个影响呢?
先利用stc-isp生成一个1ms的延时函数,然后测试一下:
#include
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--
}
}
int main()
{
while(1)
{
P2_0=0;
Delay(500);
P2_0=1;
Delay(500);
}
}
测试没有问题,那么继续写
#include
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
int main()
{
while(1)
{
if(P3_1==0)
{
Delay(20); //如果P31是按下状态,那么延时20ms
while(P3_1==0); //按下不操作,松手才操作;while(P3_1==0)检测松手,不能不加
Delay(20); //松手以后,消除松手抖动
P2_0=~P2_0; //可以操作了,然后取反
}
}
}
参照上一节写出代码,中间检查:
#include
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
int main()
{
while(1)
{
if(P3_1==0)
{
Delay(20);
while(P3_1==0); // 对1位操作
Delay(20);
// 1111 1111
P2++; // 8位总线直接操作,一开始是1111 1111,然后P2++会溢出,变为0000 0000
}
}
}
此时LED状态相反,该亮的灭,该灭的亮;那么直接取反,P2=~P2
试一下,结果发现灯都不亮
P2上电默认为1111 1111,P2++
后变为0000 0000(溢出),取反后变为1111 1111,**一直都会是这样,所以不亮,**我们可以定义一个变量,然后把变量送给P2口。
调整一下:
int main()
{
unsigned char LEDNum=0; // 无符号字符型为0~255的8位二进制数据,表示一个寄存器
while(1)
{
if(P3_1==0)
{
Delay(20);
while(P3_1==0);
Delay(20);
LEDNum=++;
P2=~LEDNum;
}
}
}
把准备工作都做好(比前两节多了一个LEDNum定义),想一下状态
可以使用位运算实现:
0000 0001 0x01<<0
0000 0010 0x01<<1
0000 0100 0x01<<2
0000 1000 0x01<<3
0001 0000 0x01<<4
0010 0000 0x01<<5
0100 0000 0x01<<6
1000 0000 0x01<<7 (这些都是正逻辑,1为亮,0为灭,一会需要取反)
可以让0-7定义为LEDNum,每按一下+1,移到最左边时再回去(if)
#include
void Delay(unsigned int xms);
unsigned char LEDNum;
int main()
{
while(1)
{
if(P3_1==0)
{
Delay(20);
while(P3_1==0);
Delay(20);
LEDNum++;
if(LEDNum>=8) // if语句如果只有一句,可以不用{}
LEDNum=0;
P2=~(0x01<<LEDNum) // P2是一个反向逻辑,给1是灭,给0是亮,所以取反
}
}
}
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
发现有一个小问题:按第一下的时候直接是D2亮而不是D1亮,给P2赋初始值P2=~0x01;
,写入主函数
int main()
{
P2=~0x01;
while(1)
{
if(P3_1==0)
{
Delay(20);
while(P3_1==0);
Delay(20);
LEDNum++;
if(LEDNum>=8) // if语句如果只有一句,可以不用{}
LEDNum=0;
P2=~(0x01<<LEDNum) // P2是一个反向逻辑,给1是灭,给0是亮,所以取反
}
}
}
如果是多个按键控制呢?如两个按键,一个左移一个右移,需要注意
#include
void Delay(unsigned int xms); //另一种声明方式
unsigned char LEDNum;
int main()
{
P2=~0x01;
while(1)
{
if(P3_1==0)
{
Delay(20);
while(P3_1==0);
Delay(20);
LEDNum++;
if(LEDNum>=8)
LEDNum=0;
P2=~(0x01<<LEDNum);
}
if(P3_0==0) // 开关是31 30 32 33
{
Delay(20);
while(P3_0==0);
Delay(20);
if(LEDNum==0) // 也需要越界判断,之前定义的是无符号char型,减到0再往下会有越界
LEDNum=7; // 先判断,才操作,如果已经为0,那么赋给最大值7
else
LEDNum--; // 不是真正的往右移,而是相对于前面的少往右移一位,相当于左移
P2=~(0x01<<LEDNum);
}
}
}
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
一位数码管的连接方式:
A~G和一个字节的八位对应;虽然这么看引脚比较乱,但是如果在图上表明的话可以发现引脚是“就近引出”
如果要显示数字6,那么就需要把AFGEDC点亮:
对应共阴极接法图中,3,8接地,然后A~DP输入10111110(也叫段码),把这八个数据给单片机的IO口上,即可显示"6"
对应共阳极接法图中,3,8接Vcc,然后A~DP输入01000001(与共阴极相反)
四位一体数码管(四个大公共端引出,其余字母相同的管子连在一块,如所有A连一块,所有B连一块):
如果想在第三位显示“1”,按共阴极接法
但这么做的话,如果其他数码管亮,也是一样的数字,怎么产生不一样的数字呢?(动态数码管显示)
C51数组
int x[3]; // 定义一组变量(3个)
int x[]={1,2,3} // 定义一组变量并初始化
x[0] // 引用数组的第0个变量
x[1] // 引用数组的第1个变量
x[2] // 引用数组的第2个变量
// 引用x=3时,数组越界,读出的数值不稳定。应该避免这种操作
C51子函数
void Function(unsigned char x, y)
{
}
返回值 函数名(形参)
{
函数体
}
数码管第三位输出“6”
#include
int main()
{
P2_4=1; // 第三位显示6,74138对应于LED6是Y5,则P24-P22输入101
P2_3=0;
P2_2=1;
P0=0x7D; // 下面P07-P00对应是0111 1101对应十六进制数为7D
while(1)
{
}
}
现在把数码管的某一位显示某一个数字封装成一个子函数
#include
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F}; // 0-9的段码表
void Nixie(unsigned char Location,Number) // 数码管显示的子函数,两个参数:位置和数字
{
switch(Location)
{
case 1: P2_4=1; P2_3=1; P2_2=1; break;
case 2: P2_4=1; P2_3=1; P2_2=0; break;
case 3: P2_4=1; P2_3=0; P2_2=1; break;
case 4: P2_4=1; P2_3=0; P2_2=0; break;
case 5: P2_4=0; P2_3=1; P2_2=1; break;
case 6: P2_4=0; P2_3=1; P2_2=0; break;
case 7: P2_4=0; P2_3=0; P2_2=1; break;
case 8: P2_4=0; P2_3=0; P2_2=0; break;
}
P0=NixieTable[Number]; // P0的值即为要显示的数字
}
int main()
{
Nixie(2,3);
while(1)
{
}
}
本节实现多个位置显示不同的数字
Nixie(2,3)
放到while循环里,复制几段(表示三位)int main()
{
while(1)
{
Nixie(1,1);
// Delay(20);
Nixie(2,2);
// Delay(20);
Nixie(3,3);
// Delay(20);
}
}
此时位置显示有点错乱,来源于数码管的常见问题,我们需要加一段消影代码
位选 段选 位选 段选 位选 段选,在下一位进行位选,上一位的段选会窜到下一位(二者紧挨着而且下一位的段选还没有到)如何避免这个问题呢?
void Nixie(unsigned char Location,Number)
{
switch(Location)
{
case 1: P2_4=1; P2_3=1; P2_2=1; break;
case 2: P2_4=1; P2_3=1; P2_2=0; break;
case 3: P2_4=1; P2_3=0; P2_2=1; break;
case 4: P2_4=1; P2_3=0; P2_2=0; break;
case 5: P2_4=0; P2_3=1; P2_2=1; break;
case 6: P2_4=0; P2_3=1; P2_2=0; break;
case 7: P2_4=0; P2_3=0; P2_2=1; break;
case 8: P2_4=0; P2_3=0; P2_2=0; break;
}
P0=NixieTable[Number];
Delay(1); // 形成数字以后先延时,否则直接清零数码管会比较暗
P0=0x00; // 清零
}
这样数码管就会显示123了,总代码如下:
#include
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
void Nixie(unsigned char Location,Number)
{
switch(Location)
{
case 1: P2_4=1; P2_3=1; P2_2=1; break;
case 2: P2_4=1; P2_3=1; P2_2=0; break;
case 3: P2_4=1; P2_3=0; P2_2=1; break;
case 4: P2_4=1; P2_3=0; P2_2=0; break;
case 5: P2_4=0; P2_3=1; P2_2=1; break;
case 6: P2_4=0; P2_3=1; P2_2=0; break;
case 7: P2_4=0; P2_3=0; P2_2=1; break;
case 8: P2_4=0; P2_3=0; P2_2=0; break;
}
P0=NixieTable[Number];
Delay(1);
P0=0x00;
}
int main()
{
while(1)
{
Nixie(1,1);
// Delay(20);
Nixie(2,2);
// Delay(20);
Nixie(3,3);
// Delay(20);
}
}
传统方式编程:所有的函数均放在main.c里,若使用的模块比较多,则一个文件内会有很多的代码,不利于代码的组织和管理,而且很影响编程者的思路
模块化编程:把各个模块的代码放在不同的.c文件里,在.h文件里提供外部可调用函数的声明,其它.c文件想使用其中的代码时,只需要#include "XXX.h"文件即可。使用模块化编程可极大的提高代码的可阅读性、可维护性、可移植性等
之前是吧Delay函数放在main函数前面,现在将Delay函数模块化。Delay.c给主函数并不需要把所有的东西都包含进去,只需要把声明包含进去。所以在.h文件中都是提供一个接口
C预编译
此外还有#ifdef,#if,#else,#elif,#undef等
其实#ifndef
等语句是对程序的某些部分是否编译进行选择
练习数码管显示函数和Delay函数的模块化
#include // <>是在安装目录里找这个文件
#include "Delay.h" // ""是在自己程序目录里寻找文件
void main()
{
while(1)
{
}
}
建立好一个Delay.c文件后,添加头文件(文件名一般与.c文件名相同)
在建立数码管显示模块时,需要加头文件(函数中用到了P2等变量,需要在同一个.c文件里面加入)
即可通过模块化轻松实现代码的简洁化:
#include
#include "Delay.h"
#include "Nixie.h"
void main()
{
while(1)
{
Nixie(1,1);
Nixie(2,2);
Nixie(3,3);
}
}
Delay.c
#include
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char i, j;
while(xms--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
Delay.h
#ifndef __DELAY_H__
#define __DELAY_H__
void Delay(unsigned int xms);
#endif
调试还有串口,数码管等。LCD1602比较方便
下面我们来试用一下,新建项目以后找到LCD1602.c和LCD1602.h文件添加的工程目录下
#ifndef __LCD1602_H__
#define __LCD1602_H__
//用户调用函数:
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
#endif
#include
//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0
//函数定义:
/**
* @brief LCD1602延时函数,12MHz调用可延时1ms
* @param 无
* @retval 无
*/
void LCD_Delay()
{
unsigned char i, j;
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
/**
* @brief LCD1602写命令
* @param Command 要写入的命令
* @retval 无
*/
void LCD_WriteCommand(unsigned char Command)
{
LCD_RS=0;
LCD_RW=0;
LCD_DataPort=Command;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602写数据
* @param Data 要写入的数据
* @retval 无
*/
void LCD_WriteData(unsigned char Data)
{
LCD_RS=1;
LCD_RW=0;
LCD_DataPort=Data;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602设置光标位置
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @retval 无
*/
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
if(Line==1)
{
LCD_WriteCommand(0x80|(Column-1));
}
else if(Line==2)
{
LCD_WriteCommand(0x80|(Column-1+0x40));
}
}
/**
* @brief LCD1602初始化函数
* @param 无
* @retval 无
*/
void LCD_Init()
{
LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
LCD_WriteCommand(0x01);//光标复位,清屏
}
/**
* @brief 在LCD1602指定位置上显示一个字符
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @param Char 要显示的字符
* @retval 无
*/
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
LCD_SetCursor(Line,Column);
LCD_WriteData(Char);
}
/**
* @brief 在LCD1602指定位置开始显示所给字符串
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串
* @retval 无
*/
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=0;String[i]!='\0';i++)
{
LCD_WriteData(String[i]);
}
}
/**
* @brief 返回值=X的Y次方
*/
int LCD_Pow(int X,int Y)
{
unsigned char i;
int Result=1;
for(i=0;i<Y;i++)
{
Result*=X;
}
return Result;
}
/**
* @brief 在LCD1602指定位置开始显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~65535
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以有符号十进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-32768~32767
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
unsigned char i;
unsigned int Number1;
LCD_SetCursor(Line,Column);
if(Number>=0)
{
LCD_WriteData('+');
Number1=Number;
}
else
{
LCD_WriteData('-');
Number1=-Number;
}
for(i=Length;i>0;i--)
{
LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以十六进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFF
* @param Length 要显示数字的长度,范围:1~4
* @retval 无
*/
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i,SingleNumber;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
SingleNumber=Number/LCD_Pow(16,i-1)%16;
if(SingleNumber<10)
{
LCD_WriteData(SingleNumber+'0');
}
else
{
LCD_WriteData(SingleNumber-10+'A');
}
}
}
/**
* @brief 在LCD1602指定位置开始以二进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
}
}
可以通过以下代码验证LCD1602的使用
#include
#include "LCD1602.h"
int main()
{
LCD_Init(); // 必须先初始化
LCD_ShowChar(1,1,'A'); // 字符用单引号
LCD_ShowString(1,3,"Hello"); // 字符串用双引号;超过一行会显示不出
LCD_ShowNum(1,9,123,3); // 指定长度如果小于给的数,从最高位缺;大于则高位补0,可自行验证
LCD_ShowSignedNum(1,13,-66,2); // 不包括符号在内有两位
LCD_ShowHexNum(2,1,0xA8,2);
LCD_ShowBinNum(2,4,0xAA,8); // 虽然是显示二进制数但是不能直接写,只能写16进制数
while(1)
{
}
}
#include
#include "LCD1602.h"
int Result;
int main()
{
LCD_Init(); // 必须先初始化
Result=1+1;
LCD_ShowNum(1,1,Result,3); // 多给一位
while(1)
{
}
}
#include
#include "LCD1602.h"
#include "Delay.h"
int Result=0;
int main()
{
LCD_Init(); // 必须先初始化
while(1)
{
Result++;
Delay(1000);
LCD_ShowNum(1,1,Result,3); // 多给一位
}
}