0、引言
51单片机的引脚有时无法满足工程对引脚数量的要求,常用的方法就是进行扩展IO口,本次分享的是我学习单片机时候学校开展的实训,主要功能是使用51单片机控制数码管和LCD1602进行计数,同时通过矩阵键盘进行控制,本文主要介绍数码管和8255A的驱动方式,并提供相关代码。首先先看一下最后的仿真图,如图1所示。
图1 仿真图
1、数码管
数码管是一种用于显示数字的电子元件,通常由多个LED(发光二极管)组成。它们通常用于时钟、计数器、温度计等设备中,可以显示数字和一些特定的符号。数码管可以是共阳极或共阴极的,共阳极的数码管在显示数字时,需要通过给对应的LED灯的阴极加电压来点亮,而共阴极的数码管则需要给对应的LED灯的阳极加电压来点亮。数码管通常由7段或14段LED组成,可以显示0-9的数字和一些字母。综上所述,数码管由LED组成,所以控制方式和控制LED的方式相同,区别在与控制数码管是同时控制多个LED以显示不同数据。本文介绍最基础的一位数码管。(多位数码管和一位数码管大同小异,可能会加上片选信号。)
共阳极数码管和共阴极数码管是两种常见的数码管类型,它们的工作原理略有不同。
共阳极数码管:在共阳极数码管中,所有的阳极都连接在一起,而每个LED的阴极则分别连接到控制电路。
共阴极数码管:在共阴极数码管中,所有的阴极都连接在一起,而每个LED的阳极则分别连接到控制电路。
两种数码管的工作原理相反,但它们都可以用于显示数字和一些特定的符号。选择使用哪种类型的数码管取决于具体的应用需求和设计考虑。图2为二者原理图。
图2 共阴极与共阳极数码管原理图
图3 一位数码管
//共阳极
char code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e}; //0-F
//共阴极
char code table[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71}; //0-F
2、8255A
8255A是一种集成电路芯片,通常被用作并行输入/输出(I/O)接口。它由英特尔(Intel)公司设计和生产,它包含了三个8位I/O端口,分别为端口A、端口B和端口C。每个端口都可以配置为输入或输出,并且可以通过编程来控制数据的传输和处理。此外,8255A还包含了一些控制寄存器,用于配置和控制I/O端口的工作模式和功能。(51单片机与8255A的连接方式可参考图1)
图4 8255A的功能结构
(1)数据总线缓冲器
D7~D0与系统数据总线相连,负责与CPU进行数据交换。包括输入输出数据、控制字和状态字。
(2)读/写控制逻辑
接收来自CPU的地址信息和控制信息。
RD:读信号输入引脚,低电平有效。
WR:写信号输入引脚,低电平有效。
(3)A组控制和B组控制
这两组控制逻辑电路接收来自CPU的控制字,控制两组端口的工作方式及读/写操作。A组控制端口A和端口C的高4位,B组控制端口B和端口C的低4位。
A1、A0:端口选择信号输入引脚。
(4)端口A、B、C
8255A有3个8位数据输入/输出端口:端口A、端口B和端口C,分别简称为A口、B口和C口。它们对外的引线分别是PA7~PA0、PB7~PB0和PC7~PC0。C口可分成两个4位的端口:C口高4位(PC7~PC4)和C口低4位(PC3~PC0)。
RESET:复位信号输入引脚,高电平有效。用于将8255A控制字寄存器清“0”,并将A、B、C口置成输入状态。
CS:片选信号输入引脚,低电平有效。
工作方式
8255A可以工作在三种不同的模式下,分别是模式0、模式1和模式2。
在模式0(基本输入输出方式)下,端口A和端口B分别作为并行输入和输出端口,而端口C可以作为两个独立的4位I/O端口。
在模式1(选通输入输出方式)下,端口A和端口B分别作为并行输入和输出端口,而端口C作为双向总线端口。
1、(选通输出)在方式1下,A口、B口都可以选通输出数据,C口的6条线作为选通控制信号:PC3、PC6、PC7(INTR、ACK、OBF)配合A口,PC0、PC1、PC2(INTR、ACK、OBF)配合B口;OBF:输出缓冲器满信号,低电平有效,当A口/B口有数据时,OBF就会变成低电平,高电平时,CPU可往A口/B口放送数据。ACK:外设收到数据的应答信号,低电平有效。INTE:中断允许状态。(工作原理:与PC7通过与门设置PC3的值,当OBF为高电平时,表示CPU可向外设传数据,但CPU迟迟为将数据传来,此时可通过PC6将INTE设置为高电平,两者通过与门后,将PC3置为高电平,向CPU发起中断请求信号(实际上是提醒CPU往外设传输数据)。INTR:中断请求信号,高电平有效。
2、(查询方式)查询端口C的BIT7,如果是0则等待;如果不是0,则CPU发送数据。
3、(中断方式)PC3置1,请求中断置位PC6,允许中断数据从端口输出,导致OBF变成高电平,证明端口数据被取走,CPU就会往端口放送数据,INTE与PC7构成一个与门,导致PC3再次置1。
在模式2(双向输入输出)下,只有A口可以工作在该模式下
根据具体的应用需求,可以选择合适的工作模式。(模式0不能工作在中断方式、模式1不能工作在无条件传输方式下)其方式控制字如图5所示。
图5 8255A控制字
附录代码
#include <reg52.h>
#include <stdio.h>
#include "absacc.h"
#define uchar unsigned char //宏定义
#define uint unsigned int //宏定义
/*
8255宏定义
A口
B口
C口
控制口
*/
#define PA XBYTE[0X0000]
#define PB XBYTE[0X0400]
#define PC XBYTE[0X0800]
#define PCTL XBYTE[0X0C00]
/*
1602引脚定义
*/
sbit LcdRs_P = P2^6; // 1602液晶的RS管脚
sbit LcdRw_P = P2^5; // 1602液晶的RW管脚
sbit LcdEn_P = P2^4; // 1602液晶的EN管脚
code P1_scan[]={0x7f,0xbf,0xdf,0xef}; //按键扫描数组
code key_temp_value[]={0xee,0xed,0xeb,0xe7,0xde,0xdd,0xdb,0xd7,0xbe,0xbd,0xbb,0xb7,0x7e,0x7d,0x7b,0x77}; //按键按下所对应的值
code uchar tmpled[8] = {0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f};
uchar duan[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; //数码管
uchar wei[] ={0xfe,0xfd,0xfb,0xf7,0xef,0xdf};
uchar count = 0; //全局变量,控制定时次数,count=20,为定时1s到了
uchar hour,min,sec;
uchar display_time[6] = {0,0,0,0,0,0};
//定时器0初始化函数
void Timer0_Init(void) //50毫秒@12.000MHz
{
TMOD = 0x01; //设置定时器模式-定时器0工作在模式1,16位
TL0 = (65536-50000)%256; //设置定时初值
TH0 = (65536-50000)/256; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
}
/*延时函数*/
void delay(uint x)
{
uchar i;
while(x--)
{
for(i=0;i<120;i++);
}
}
//显示函数
void display()
{
uchar i;
display_time[0] = hour/10;
display_time[1] = hour%10;
display_time[2] = min/10;
display_time[3] = min%10;
display_time[4] = sec/10;
display_time[5] = sec%10;
for (i=0;i<6;i++)
{
//调试时发现,对于共阳极数码管,先送段码,后送位码,显示不正确。
PB = wei[i];
if (i==1||i==3)
PA = duan[display_time[i]]+0x80;
else
PA = duan[display_time[i]];
delay(2);
}
}
//时间调整函数
void time_adjust(uchar pos)
{
if(pos==0)
{
hour++; if(hour>=24) hour = 0;
}
if(pos==1)
{
if (hour==0) hour = 23;
else hour--;
}
if(pos==2)
{
min++; if(min>=60) min = 0;
}
if(pos==3)
{
if(min==0) min = 59;
else min--;
}
if(pos==4)
{
sec++; if(sec>=60) sec = 0;
}
if(pos==5)
{
if(sec==0) sec = 59;
else sec--;
}
if(pos==6)
{
TR0 = 1;
}
if(pos==7)
{
TR0 = 0;
}
}
//键盘扫描函数,此函数应该修改为按下抬起后有效
void key_scan()
{
uchar i,j;
for(i=0;i<4;i++) //让每个列线出现低电平(按键扫描数据)
{
P1=P1_scan[i];
if(P1!=P1_scan[i])
{
delay(3); //消抖
for (j=0;j<16;j++)
{
if (P1==key_temp_value[j])
{
while(P1==key_temp_value[j]);
time_adjust();
}
}
}
}
}
/*********************************************************/
// 毫秒级的延时函数,time是要延时的毫秒数
/*********************************************************/
void DelayMs(uint time)
{
uint i,j;
for(i=0;i<time;i++)
for(j=0;j<112;j++);
}
/*********************************************************/
// 1602液晶写命令函数,cmd就是要写入的命令
/*********************************************************/
void LcdWriteCmd(uchar cmd)
{
LcdRs_P = 0;
LcdRw_P = 0;
LcdEn_P = 0;
PC=cmd;
DelayMs(2);
LcdEn_P = 1;
DelayMs(2);
LcdEn_P = 0;
}
/*********************************************************/
// 1602液晶写数据函数,dat就是要写入的数据
/*********************************************************/
void LcdWriteData(uchar dat)
{
LcdRs_P = 1;
LcdRw_P = 0;
LcdEn_P = 0;
PC=dat;
DelayMs(2);
LcdEn_P = 1;
DelayMs(2);
LcdEn_P = 0;
}
/*********************************************************/
// 液晶光标定位函数
/*********************************************************/
void LcdGotoXY(uchar line,uchar column)
{
// 第一行
if(line==0)
LcdWriteCmd(0x80+column);
// 第二行
if(line==1)
LcdWriteCmd(0x80+0x40+column);
}
/*********************************************************/
// 液晶输出字符串函数
/*********************************************************/
void LcdPrintStr(uchar *str)
{
while(*str!='\0')
LcdWriteData(*str++);
}
//平方
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;
LcdGotoXY(Line,Column);
for(i=Length;i>0;i--)
{
LcdWriteData(Number/LCD_Pow(10,i-1)%10+'0');
}
}
/*********************************************************/
// 1602液晶功能初始化
/*********************************************************/
void LcdInit()
{
LcdWriteCmd(0x38); // 16*2显示,5*7点阵,8位数据口
LcdWriteCmd(0x0C); // 开显示,不显示光标
LcdWriteCmd(0x06); // 地址加1,当写入数据后光标右移
LcdWriteCmd(0x01); // 清屏
}
/*********************************************************/
// 开启LCD1602显示
/*********************************************************/
void LcdOn() //开显示器,关光标与闪烁
{
LcdWriteCmd(0x0c);
}
/*********************************************************/
//关闭LCD1602显示,但DDRAM中内容不丢失,重开后将恢复内容
/*********************************************************/
void LcdOff()
{
LcdWriteCmd(0x08);
}
/*********************************************************/
// 1602液晶显示内容初始化
/*********************************************************/
void LcdShowInit()
{
//LcdGotoXY(0,0); // 定位到第0行第0列
//LcdPrintStr(" sec "); // 第0行显示“ ”
//LcdGotoXY(1,0); // 定位到第1行第0列
//LcdPrintStr("ABCDEFGHIJKLMNOP"); // 第1行显示“ ”
}
void int_timer0(void) interrupt 1 //定时器0中断服务程序
{
TL0 = (65536-50000)%256; //设置定时初值
TH0 = (65536-50000)/256; //设置定时初值
count++;
if (count==20)
{
count = 0;
sec++;
if (sec>=60)
{
sec = 0;
min++;
if (min>=60)
{
min = 0;
hour++;
if (hour>=24)
{
hour = 0;
}
}
}
}
}
/*
函数名称:主函数
作者:
时间:2024年1月5日
修改:无
*/
void main(void)
{
EA = 1;
ET0 = 1;
PCTL = 0x80; //设置8255工作方式
LcdInit(); // 液晶功能初始化
Timer0_Init();
LcdShowInit();
LcdGotoXY(0,2); // 定位
LcdPrintStr(":"); // 第1行显示
LcdGotoXY(0,5); // 定位
LcdPrintStr(":"); // 第1行显示
while(1)
{
key_scan();
display();
LCD_ShowNum(0,0,hour,2);
LCD_ShowNum(0,3,min,2);
LCD_ShowNum(0,6,sec,2);
}
}
本文正好借助我实训的任务,给大家介绍一下8255和数码管的相关知识,矩阵键盘、LCD1602等相关知识寒假期间我也会逐步上传至平台,期待和大家一起进步!