参考资料
51单片机入门教程-2020版 程序全程纯手打 从零开始入门
江协科技资料下载
51单片机(STC89C52RC)系统性学习笔记
1. 准备工作
1.1 win10配置51单片机开发环境
- 51单片机
正在重新握手 ... 成功 [0.578"]
当前的波特率: 115200
正在擦除目标区域 ... 完成 ! [0.328"]
正在下载用户代码 ... 完成 ! [0.141"]
正在设置硬件选项 ... 完成 ! [0.031"]
更新后的硬件选项为:
. 当前的时钟频率: 11.088MHz
. 系统频率为12T(单倍速)模式
. 振荡器的放大增益不降低
. 当看门狗启动后,任何复位都可停止看门狗
. MCU内部的扩展RAM可用
. ALE脚的功能选择仍然为ALE功能脚
. P1.0和P1.1与下次下载无关
. 下次下载用户程序时,不擦除用户EEPROM区
单片机型号: STC89C52RC/LE52RC
固件版本号: 6.6.4C
编程工具
Keil5 C51
新建项目时选择AT89C51RC2
将程序载入到51单片机的工具
STC-ISP
51单片机接入电脑,并配置驱动CH340_CH341
1.1 Ubuntu配置51单片机开发环境
- 编辑器 VS Code 安装教程
- 安装VS Code插件
C/C++ Extension Pack
- 安装编译工具
sudo apt-get install sdcc
- 程序编译
sdcc 1_LED_1.c
将编译生成的文件输出到out目录,需要先通过sudo mkdir out
创建out目录
sdcc 1_LED_1.c -o out/
问题1:mcs51/8051.h
依赖于mcs51/lint.h
#include <mcs51/lint.h>
#include <mcs51/8051.h>
问题2:提示找不到头文件mcs51/8051.h
8051.h
是安装sdcc
后产生的,请先确保sdcc
已经安装,sudo find / -name 8051.h
搜索一下头文件8051.h
所在的目录
这里目录是/usr/share/sdcc/include
,将该路径配置到includepath
,先点击Quick Fix
再点击Edit “includePath” setting
添加/usr/share/sdcc/include
到Include path
2. 认识51单片机
2.1 STC89C52RC单片机
- 所属系列:51单片机系列
- 公司:STC公司
- 位数:8位
- RAM:512字节
- ROM:4K(Flash)
- 工作频率:11.088MHz(本开发板使用)
2.2 管脚图
2.3 原理图
高清PDF可从这里下载江协科技资料下载
2.4 按键抖动
对于机械开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开,所以在开关闭合及断开的瞬间会伴随一连串的抖动。
因此,需要进行防抖处理,如下:
// 这是一个检测独立按键K1是否按下,并控制LED模块的简化代码
if(P3_1==0) // 如果K1按键按下,P3_1的值会变为0
{
Delay(20); // 延时20毫秒消抖
while(P3_1==0); // 死循环空转,直到K1按键松开,P3_1变为1,跳出循环
Delay(20); // 延时20毫秒消抖
P2_0=~P2_0; // LED1的亮灭情况
}
2.5 头文件说明
#include <mcs51/lint.h>
#include <mcs51/8051.h>
#include <REGX52.H>
2.6 模块化函数
2.6.1 延时函数
#ifndef __DELAY_H__
#define __DELAY_H__
void Delayms(unsigned int k);
#endif
#include <INTRINS.H>
// 延时k ms
void Delayms(unsigned int k) //@11.0592MHz
{
unsigned char i, j;
while (k --) {
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
2.6.2 矩阵键盘
#ifndef __MATRIXKEYBOARD_H__
#define __MATRIXKEYBOARD_H__
unsigned int matrixKeyboard();
#endif
#include <at89c51RC2.h>
#include "Delay.h"
// 反回键码(1 ~ 16),行优先遍历
// 按列扫描矩阵键盘
// 如果按行的话,P1_5和蜂鸣器冲突,会导致其发声
unsigned int matrixKeyboard() {
unsigned int keyNum = 0;
P1 = 0xFF;
P1_3 = 0; // 第一列
if (P1_7 == 0) {Delayms(20); while (P1_7 == 0); Delayms(20); keyNum = 1;}
if (P1_6 == 0) {Delayms(20); while (P1_6 == 0); Delayms(20); keyNum = 5;}
if (P1_5 == 0) {Delayms(20); while (P1_5 == 0); Delayms(20); keyNum = 9;}
if (P1_4 == 0) {Delayms(20); while (P1_4 == 0); Delayms(20); keyNum = 13;}
P1 = 0xFF;
P1_2 = 0; // 第二列
if (P1_7 == 0) {Delayms(20); while (P1_7 == 0); Delayms(20); keyNum = 2;}
if (P1_6 == 0) {Delayms(20); while (P1_6 == 0); Delayms(20); keyNum = 6;}
if (P1_5 == 0) {Delayms(20); while (P1_5 == 0); Delayms(20); keyNum = 10;}
if (P1_4 == 0) {Delayms(20); while (P1_4 == 0); Delayms(20); keyNum = 14;}
P1 = 0xFF;
P1_1 = 0; // 第三列
if (P1_7 == 0) {Delayms(20); while (P1_7 == 0); Delayms(20); keyNum = 3;}
if (P1_6 == 0) {Delayms(20); while (P1_6 == 0); Delayms(20); keyNum = 7;}
if (P1_5 == 0) {Delayms(20); while (P1_5 == 0); Delayms(20); keyNum = 11;}
if (P1_4 == 0) {Delayms(20); while (P1_4 == 0); Delayms(20); keyNum = 15;}
P1 = 0xFF;
P1_0 = 0; // 第四列
if (P1_7 == 0) {Delayms(20); while (P1_7 == 0); Delayms(20); keyNum = 4;}
if (P1_6 == 0) {Delayms(20); while (P1_6 == 0); Delayms(20); keyNum = 8;}
if (P1_5 == 0) {Delayms(20); while (P1_5 == 0); Delayms(20); keyNum = 12;}
if (P1_4 == 0) {Delayms(20); while (P1_4 == 0); Delayms(20); keyNum = 16;}
return keyNum;
}
3. LED模块
3.1 原理图
LED等的左侧接入VCC正极,若要使灯亮,则右侧需要接入负极,也就是对应的寄存器位需要赋值为0。
实验1:点亮LED灯D1
#include <AT89C51RC2.h>
/**
* 点亮LED灯D1
*/
void main() {
P2 = 0xFF; // 将所有灯熄灭
P2_0 = 0; // D1灯亮起
while (1); // 程序始终保持运行
}
实验2:LED灯D8闪烁
#include <AT89C51RC2.h>
#include <INTRINS.H>
/**
* LED灯D8闪烁
*/
void main() {
P2 = 0xFF;
while (1)
{
P2_7 = 0; // D8灯亮
Delayms(1000); // 延迟1000ms = 1s
P2_7 = 1; // D8灯灭
Delayms(1000); // 延迟1000ms = 1s
}
}
实验3:LED流水灯
#include <AT89C51RC2.h>
#include "Delay.h"
/**
* LED灯D1 ~ D8按顺序点亮,每次只有一个灯亮
*/
void main() {
// 1111 1110
// 1111 1101
// 1111 1011
// ...
// 1111 1110
// =》 其实就是实现循环左移
P2 = 0xFE;
while (1)
{
P2 <<= 1;
if (P2 != 0xFE) P2 |= 0x01;
Delayms(1000); // 延迟1000ms = 1s
}
}
4. 独立按键模块
4.1 原理图
实验1:独立按键控制LED灯D1
#include <AT89C51RC2.h>
#include "Delay.h"
/**
* 独立按键K1控制LED灯D1
* 按下按键并松开后,变换灯D1的状态
*/
void main() {
while (1)
{
// 注意控制K1的是P3_1,控制K2的是P3_0
// 当值为0是代表按键按下
if (P3_1 == 0)
{
Delay(20); // 消除按键抖动
while (P3_1 == 0); // 一直保持按下的状态则卡在这
Delay(20); // 消除按键抖动
P2_0 = ~P2_0; // 状态取反
}
}
}
5. 动态数码管模块
5.1 原理图
LED1 ~ LED8的亮灭(位选)由P24, P23, P22三位决定,P24位高位,当这三个位表示数据n时,Yn对应的灯亮,Y0对应LED1
P0_0 ~ P0_6分别控制a ~ g晶体管,P0_7控制dp晶体管,为1代表选中。(P0_7是最高位,P0_0是最低位)
实验1:数码管LED3显示2
#include <AT89C51RC2.h>
/**
* 数码管LED3显示2
*/
void main() {
// 选中数码管LED3,对应Y2 P2_4 P2_3 P2_2 = 0 1 0
P2_4 = 0;
P2_3 = 1;
P2_2 = 0;
// a b c d e f g dp
// 0 1 2 3 4 5 6 7
// a b c d e f g dp
// 1 1 0 1 1 0 1 0 => 0101 1011 = 0x5B
// 数字2,需要点亮 a b g e d
P0 = 0x5B;
while (1);
}
实验2:数码管LED8 ~ LED1分别显示0 ~ 8
// 对应数字 0 ~ 9 的段码
unsigned char NixieTable[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};
// 在LED-loc 显示num
void Nixie(unsigned char loc, unsigned char num) {
P2_4 = 0; P2_3 = 0; P2_2 = 0; // 1110 0011 = 0xE3 P2 &= 0xE3
if ((loc & 0x01) == 1) P2_2 = 1;
if ((loc & 0x02) == 2) P2_3 = 1;
if ((loc & 0x04) == 4) P2_4 = 1;
P0 = NixieTable[num];
// 消影,数字清零,防止上一个位置的数字显示到下一个位置
Delayms(1);
P0 = 0x00;
}
/**
* 数码管LED8 ~ LED1分别显示0 ~ 8
**/
void main() {
unsigned char i;
while (1) {
for (i = 0; i <= 7; i ++) {
Nixie(7 - i, i);
}
}
}
6. LCD1602接口
6.1 原理图
7. 矩阵按键模块
7.1 原理图
实验1:在LCD1602显示按下的键码(1 ~ 16)
#include <at89c51RC2.h>
#include "Delay.h"
// 反回键码(1 ~ 16),行优先遍历
// 没有按下任何键则返回0
// 按列扫描矩阵键盘
// 如果按行的话,P1_5和蜂鸣器冲突,会导致其发声
unsigned int matrixKeyboard() {
unsigned int keyNum = 0;
P1 = 0xFF;
P1_3 = 0; // 第一列
if (P1_7 == 0) {Delayms(20); while (P1_7 == 0); Delayms(20); keyNum = 1;}
if (P1_6 == 0) {Delayms(20); while (P1_6 == 0); Delayms(20); keyNum = 5;}
if (P1_5 == 0) {Delayms(20); while (P1_5 == 0); Delayms(20); keyNum = 9;}
if (P1_4 == 0) {Delayms(20); while (P1_4 == 0); Delayms(20); keyNum = 13;}
P1 = 0xFF;
P1_2 = 0; // 第二列
if (P1_7 == 0) {Delayms(20); while (P1_7 == 0); Delayms(20); keyNum = 2;}
if (P1_6 == 0) {Delayms(20); while (P1_6 == 0); Delayms(20); keyNum = 6;}
if (P1_5 == 0) {Delayms(20); while (P1_5 == 0); Delayms(20); keyNum = 10;}
if (P1_4 == 0) {Delayms(20); while (P1_4 == 0); Delayms(20); keyNum = 14;}
P1 = 0xFF;
P1_1 = 0; // 第三列
if (P1_7 == 0) {Delayms(20); while (P1_7 == 0); Delayms(20); keyNum = 3;}
if (P1_6 == 0) {Delayms(20); while (P1_6 == 0); Delayms(20); keyNum = 7;}
if (P1_5 == 0) {Delayms(20); while (P1_5 == 0); Delayms(20); keyNum = 11;}
if (P1_4 == 0) {Delayms(20); while (P1_4 == 0); Delayms(20); keyNum = 15;}
P1 = 0xFF;
P1_0 = 0; // 第四列
if (P1_7 == 0) {Delayms(20); while (P1_7 == 0); Delayms(20); keyNum = 4;}
if (P1_6 == 0) {Delayms(20); while (P1_6 == 0); Delayms(20); keyNum = 8;}
if (P1_5 == 0) {Delayms(20); while (P1_5 == 0); Delayms(20); keyNum = 12;}
if (P1_4 == 0) {Delayms(20); while (P1_4 == 0); Delayms(20); keyNum = 16;}
return keyNum;
}
// 在LCD1602显示按下的键码(1 ~ 16)
void main() {
unsigned int num = 0, newNum = 0;
LCD_Init();
while (1) {
newNum = matrixKeyboard();
if (newNum != 0) {
num = newNum;
}
LCD_ShowNum(1, 1, num, 2); // 第一行第一列显示,占两个位置
}
}
实验2:4位密码锁
// 10为0,11为确认,12为清空
void main() {
// num,保存当前输入的密码
// keyNum,记录矩阵键盘按下的键码
// count,记录当前已经输入多少个数字
unsigned int num = 0, keyNum = 0, count = 0;
// LCD1602初始化
LCD_Init();
// 在第一行的前9个输入固定显示输入提示符
LCD_ShowString(1, 1, "PASSWORD:");
while (1) {
// 第10个字符显示当前输入的密码
LCD_ShowNum(1, 10, num, 4);
// 获取当前矩阵键盘的键码(没有按下任何键则返回0)
keyNum = matrixKeyboard();
if (keyNum >= 1 && keyNum <= 9 && count < 4) {
num = num * 10 + keyNum;
count ++;
} else if (keyNum == 11 && count == 4) {
if (num == 1234) {
LCD_ShowString(2, 1, "OK");
} else {
LCD_ShowString(2, 1, "ERR");
}
} else if (keyNum == 12) {
num = 0;
count = 0;
LCD_ShowString(2, 1, " ");
}
}
}
8. 定时器与中断
定时器介绍:51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成
定时器作用︰
(1)用于计时系统,可实现软件计时,或者使程序每隔一固定时间完成一项操作
(2)替代长时间的Delay,提高CPU的运行效率和处理速度(…)
8.1 原理图
TL0和TH0发生溢出时(达到65535),TF0标志位被置位1,从而引发中断。
- 生成定时器初始化代码
void Timer0Init(void) //1毫秒@11.0592MHz
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
// 这两个开关配置需要自己加一下
ET0 = 1; // 打开定时器0
EA = 1; // 打开总开关
}
实验1:实现一个电子时钟
void Timer0Init() //1毫秒@11.0592MHz
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; // 打开定时器0
EA = 1; // 打开总开关
}
unsigned int count = 0;
// 设置时钟为 23:59:45 方便调试
unsigned int second = 45;
unsigned int minute = 59;
unsigned int hour = 23;
void main() {
// // TMOD不可位寻址, M0给1 => 0x01
// // TMOD = 0x01; // 配置定时器0
// // 防止对定时器1的配置产生影响,使用与或赋值法
// TMOD &= 0xF0; // 清空定时器0的配置
// TMOD |= 0x01; // 配置定时器0
//
// // TCON可位寻址,TF0 = 0, TR0 = 1
// TF0 = 0;
// TR0 = 1;
//
// // 64635 到 65535 相差1000,也就是1ms会发生中断
// TH0 = 64535 / 256; // 取高8位
// TL0 = 54535 % 256; // 取低8位
//
// // 定时器开关
// ET0 = 1; // 打开定时器0
// EA = 1; // 打开总开关
PT0 = 0;
Timer0Init();
LCD_Init();
LCD_ShowChar(1, 3, ':');
LCD_ShowChar(1, 6, ':');
while (1) {
if (second == 60) {
second = 0;
minute ++;
}
if (minute == 60) {
minute = 0;
hour ++;
}
if (hour == 24) {
hour = 0;
}
LCD_ShowNum(1, 1, hour, 2);
LCD_ShowNum(1, 4, minute, 2);
LCD_ShowNum(1, 7, second, 2);
}
}
// 发生时钟中断时,会触发该函数,中断号为1
void Timer0_Routube() interrupt 1 {
TL0 = 0x66; //设置定时初值
TH0 = 0xFC; //设置定时初值
count ++;
if (count == 1000) {
second ++;
count = 0;
// 中断程序应该尽量简短
// if (second == 60) {
// second = 0;
// minute ++;
// if (minute == 60) {
// minute = 0;
// hour ++;
// if (hour == 24) {
// hour = 0;
// }
// }
// }
}
}
9. 串口通信
9.1 原理图
使用波特率计算器生成定时器1的配置,用于设置接收和发送速度
// 配置定时器1用于串口的同步通讯,控制波特率 void UartInit(void) //4800bps@11.0592MHz { PCON &= 0x7F; //波特率不倍速 // 方式1(SM0 = 0; SM1 = 1) 允许接收(REN = 1) SCON = 0x50; //8位数据,可变波特率 AUXR &= 0xBF; //定时器1时钟为Fosc/12,即12T AUXR &= 0xFE; //串口1选择定时器1为波特率发生器 TMOD &= 0x0F; //清除定时器1模式位 TMOD |= 0x20; //设定定时器1为8位自动重装方式 TL1 = 0xFA; //设定定时初值 TH1 = 0xFA; //设定定时器重装值 ET1 = 0; //禁止定时器1中断 TR1 = 1; //启动定时器1 // 当发送缓存区满和接收缓冲区满都会触发串口中断,中断号为4 EA = 1; // 中断允许控制位 ES = 1; // 允许串口中断 }
使用串口助手测试串口通信功能
注意串口需要与左半部分的串口号保持一致,波特率需要与定时器1的配置保持一致。
实验1:每隔一秒发送一个数据,并且将接收字节保存到P2,控制LED灯
#include <AT89C51RC2.h>
#include <INTRINS.H>
#include "Delay.h"
// 配置定时器1用于串口的同步通讯,控制波特率
void UartInit(void) //4800bps@11.0592MHz
{
PCON &= 0x7F; //波特率不倍速
// 方式1(SM0 = 0; SM1 = 1) 允许接收(REN = 1)
SCON = 0x50; //8位数据,可变波特率
AUXR &= 0xBF; //定时器1时钟为Fosc/12,即12T
AUXR &= 0xFE; //串口1选择定时器1为波特率发生器
TMOD &= 0x0F; //清除定时器1模式位
TMOD |= 0x20; //设定定时器1为8位自动重装方式
TL1 = 0xFA; //设定定时初值
TH1 = 0xFA; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
// 当发送缓存区满和接收缓冲区满都会触发串口中断,中断号为4
EA = 1; // 中断允许控制位
ES = 1; // 允许串口中断
}
// 串口发送一个字节的数据
void sendByte(unsigned char buffer) {
SBUF = buffer; // 发送数据
while (TI == 0); // 等待数据发送完成
TI = 0; // 复位
}
void main() {
unsigned char buffer = 0x00;
// 初始化串口
UartInit();
while (1) {
sendByte(buffer);
buffer ++;
Delayms(1000);
}
}
// 串口中断程序,中断号为4
// 发送缓存区满和接收缓冲区满都会触发该中断
// 接收中断触发时,RI = 1;发送中断触发时,TI = 1
// 处理完中断后,需要将手动将RI或者TI复位为0,否则会重复触发串口中断
// 用接收的数据控制P2寄存器,也就是LED灯
void UARTRoutine() interrupt 4 {
unsigned char buffer = SBUF;
if (RI == 1) { // 串口接收中断
P2 = SBUF;
RI = 0;
}
// if (TI == 1) // 串口发送中断
}