注:此单片机型号为 STC15F2K60S2.
原理
STC15F2K60S2的数码管是八位数码管,首先先看他们的接口。
com 接口:位选接口,用来选择第1—8位数码管,一次只能选中一个。当com接口为1时,选中。
a——dp 接口:段选接口,用来选择数码管上的对应横竖LED灯管。当为0时,选中。dp为右下角的点。
注:所有的数码管的a—dp接口都是连在一起的。
位选:
以第一个数码管为例,要选中第一个数码管,就要使得com1为1,可控管脚为P00—P07,所以要输入P00为高电平。为了让P00能够影响到com1,就要使得锁存器 M74HC573M1R 导通,即要让管脚LE为高电平。又管脚LE与Y6C连接,所以Y6C也为高电平。
又Y6C连接了一个 或非门,或非门的输入端为Y6和WR。因为WR已用跳线帽与GND连接在了一起,GND为接地,所以WR为0(低电平)。
根据或非门逻辑表格:
Y6 WR Y6C
0 0 1
0 1 0
1 0 0
1 1 0
所以,当Y6C为0时,Y6为1;当Y6C为1时,Y6为0.
所以,要使得Y6C为高电平,Y6就要为低电平,即Y6为0.
又Y6是由 74HC138 位译码器的输入端A、B、C来控制,以 C、B、A 为逻辑顺序,CBA组成二进制数,来选中Y0—Y7,被选中者即为低电平。
所以,要使得 Y6 为低电平,CBA 要为 110.
另外,由于 P0 口是复用的,意味着也要控制别的器件,所以如果一直让 LE 为高,那么就在控制别的器件的时候就会影响到 LED,所以在锁存器导通后,应该截止保存住它的状态。
段选:
以点亮数字“1”为例,当要点亮数字“1”时,‘b1’‘c1’被选中,为低电平,其余则为高电平。
由于’a1’—‘dp1’与‘a’—‘dp’相连,a’—‘dp’又通过锁存器M74HC573M1R与P0口相连。
所以,要选中’b’‘c’,则P0=0xF9. //1111 1001
且还要锁存器导通,即LE=1,即Y7C=1.
又Y7C通过或非门与Y7相连,所以Y7为0,所以CBA为111.
整合逻辑:
段选:
com1 -> P0 = 0x01 ->LE=1 -> Y6C=1 ->
Y6=0 -> CBA=110 ->锁存CBA = 000
位选:
valus = 1 -> 选中b,c -> P0 = 0xF9 -> LE=1 ->
Y7C=1 -> Y7=0 -> CBA=111 ->锁存CBA = 000
代码
蓝桥杯官方代码。
“seg.c”
#include "seg.h"
// 显示转换
void Seg_Tran(unsigned char *pucSeg_Buf, unsigned char *pucSeg_Code)
{
unsigned char i, //i为段选数,com i
j=0, temp;
for(i=0; i<8; i++, j++)
{
switch(pucSeg_Buf[j])
{ // 低电平点亮段,段码[MSB...LSB]对应码顺序为[dp g f e d c b a] //LSB最低有效位和MSB最高有效位
case '0': temp = 0xc0; break;
case '1': temp = 0xf9; break;
case '2': temp = 0xa4; break;
case '3': temp = 0xb0; break;
case '4': temp = 0x99; break;
case '5': temp = 0x92; break;
case '6': temp = 0x82; break;
case '7': temp = 0xf8; break;
case '8': temp = 0x80; break;
case '9': temp = 0x90; break;
case 'A': temp = 0x88; break;
case 'B': temp = 0x83; break;
case 'C': temp = 0xc6; break;
case 'D': temp = 0xA1; break;
case 'E': temp = 0x86; break;
case 'F': temp = 0x8E; break;
case 'H': temp = 0x89; break;
case 'L': temp = 0xC7; break;
case 'N': temp = 0xC8; break;
case 'P': temp = 0x8c; break;
case 'U': temp = 0xC1; break;
case '-': temp = 0xbf; break;
case ' ': temp = 0xff; break;
default: temp = 0xff;
}
if(pucSeg_Buf[j+1] == '.')
{
temp = temp&0x7f;
j++;
}
pucSeg_Code[i] = temp ;
}
}
// 数码管显示
void Seg_Disp(unsigned char *pucSeg_Code, unsigned char ucSeg_Pos)
{
P0 = 0xff; // 消隐
P2 = P2 & 0x1F | 0xE0; // P27~P25清零,再定位Y7C
P2 &= 0x1F; // P27~P25清零
P0 = 1<<ucSeg_Pos; // 位选
P2 = P2 & 0x1F | 0xC0; // P27~P25清零,再定位Y6C
P2 &= 0x1F; // P27~P25清零,锁存
P0 = pucSeg_Code[ucSeg_Pos]; // 段码(段选,从段码数组中选择需要的字符)
P2 = P2 & 0x1F | 0xE0; // P27~P25清零,再定位Y7C
P2 &= 0x1F; // P27~P25清零
}
“tim.c”
#include "tim.h"
void Cls_Peripheral(void) //关闭外设。
{
P0 = 0xFF;
P2 = P2 & 0x1F | 0x80; // P27~P25清零,再定位Y4C
P2 &= 0x1F; // P27~P25清零
P0 = 0;
P2 = P2 & 0x1F | 0xA0; // P27~P25清零,再定位Y5C
P2 &= 0x1F; // P27~P25清零
}
void Led_Disp(unsigned char ucLed) //LED灯点亮
{ // IO模式(J13-2和J13-3相连)
P0 = ~ucLed;
P2 = P2 & 0x1F | 0x80; // P27~P25清零,再定位Y4C
P2 &= 0x1F; // P27~P25清零
//XBYTE[0x8000] = ~ucLed; // MM模式(J13-2和J13-1相连)
}
void Timer1Init(void) // 1毫秒@12.000MHz
{
AUXR &= 0xBF; // 定时器1时钟12T模式,AUXR.1清零
TMOD &= 0x0F; // 设置定时器模式,软件驱动,定时器模式,m0m1为00排列,模式0
TL1 = 0x18; // 设置定时初值 (65536 - 1000) % 256
TH1 = 0xFC; // 设置定时初值 (65536 - 1000) / 256
TF1 = 0; // 清除TF1标志,溢出标志位
TR1 = 1; // 定时器1开始计时
ET1 = 1; // 允许定时器1中断
EA = 1; // 允许系统中断
}
main.c
// 运行程序时,将J13调整为io模式(1-2脚短接)
#include "tim.h"
#include "seg.h"
#include "stdio.h"
unsigned char ucSec, ucLed;
unsigned char pucSeg_Buf[9], //字符型段选数组
pucSeg_Code[8], //字符型位选数组
ucSeg_Pos; //位选数\段选数
unsigned long ulms;
// 注意:sprintf()会在字符串后面添加”\0”,所以pucSeg_Buf[]的长度应为9。
// 如果字符串中包含小数点,pucSeg_Buf[]的长度应为10。
void main(void)
{
Cls_Peripheral(); //关闭外设
Timer1Init(); //定时器1初始化
while(1)
{
sprintf(pucSeg_Buf, " %04u", (unsigned int)ucSec); // %u 十进制无符号整数 ,向BUF数组里写入数据
Seg_Tran(pucSeg_Buf, pucSeg_Code);//数码管转换
}
}
void Time_1(void) interrupt 3
{
ulms++;
if(!(ulms % 1000))
{
ucSec++;//每秒加1
ucLed ^= 1;//ucled第一位取反
Led_Disp(ucLed);
}
Seg_Disp(pucSeg_Code, ucSeg_Pos); //数码管显示
if(++ucSeg_Pos == 8) ucSeg_Pos = 0;//当位选数为8时,清零(保持在0-7)
}
注释代码参考博客:
@love、reading——C 字符串函数 sprintf ()、snprintf () 详解
@myyllove——C/C++ % s % d % u 基本概念与用法
我自己的代码
单个数码管显示——实现数码管显示流水灯的对应位置
seg.c
/****************************************************************************
* Copyright (C), 2022,Moqim
* 文件名: seg.c
* 内容简述:数码管显示函数
*
* 文件历史:
* 版本号 日期 作者 说明
* 01a 2022-04-26 Moqim 创建该文件
* 01b 2022-05-01 Moqim 使案例简单化
*/
#include "seg.h"
/* 定义全局变量,代表第一个数码管的字码值 */
unsigned char smg1;
/*数据选择区*/
/*"code"关键字,表示该数据不可修改*/
/* 0、1、2、3、4、5、6、7、8、9 */
unsigned char code Nixie[] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90};
/****************************************************************************
* 函数名: SEG_Scan()
* 功 能: 动态点亮数码管
* 输 入: 无
* 输 出: 无
* 说 明:在数码管动态显示过程中,主要是在数码管位选和段选产生的瞬态造成的。
数码管在不应该亮的地方有点微微发亮,这种现象就是 “鬼隐”。
消除鬼隐(简称“消隐”)的办法:
关闭段:在改变值之前,使数码管全部熄灭。
关闭位:关闭数码管的位,等到赋值过程都做好了后,再重新打开。
*/
void SEG_Scan(void)
{
/* 消隐 */
P0 = 0xFF;
P2 = P2 & 0x1F | 0xE0;
P2 &= 0x1F;
/* 位选 */
P0 = 0x01; //选择第一个数码管
P2 |= 0xC0;
P2 &= 0x1F;
/* 段选 */
P0 = 0xFF; //消隐
P2 = P2 & 0x1F | 0xE0;
P0 = Nixie[smg1]; //选择字码
P2 &= 0x1F;
}
seg.h
#ifndef __SEG_H
#define __SEG_H
#include "stc15f2k60s2.h"
/* 声明变量,使其可以被调用该头文件的文件调用 */
extern unsigned char smg1;
void SEG_Scan(void);
#endif /*__SEG_H*/
main.c
//led.c文件参考上一篇博客:蓝桥杯 —— 单片机学习(3—— 点亮 LED 灯)
/****************************************************************************
* Copyright (C), 2022,Moqim
* 文件名: main.c
* 内容简述:实现数码管显示流水灯的对应位置
*
* 文件历史:
* 版本号 日期 作者 说明
* 01a 2022-04-26 Moqim 创建该文件
* 01b 2022-05-01 Moqim 修改main函数
*/
//头文件调用区
#include "stc15f2k60s2.h"
#include "led.h"
#include "seg.h"
//变量定义区
int i = 0;
//函数声明区
void Delay_ms(unsigned int num);
//Main Body
int main(void)
{
LED_Init();
while (1)
{
LED_Ctrl(0x01 << i); //流水灯
smg1 = i + 1; //赋值数码管字码
SEG_Scan(); //数码管显示
i++;
if (i == 8)i = 0; //范围限制
Delay_ms(1000); //延时1s
}
}
/****************************************************************************
* 函数名: Delay_ms()
* 功 能: 延时
* 输 入: unsigned int num:延时num毫秒
* 输 出: 无
*/
void Delay_ms(unsigned int num)//软件延时不精确,凑合用吧
{
int i,j;
for(i = 0;i<num;i++)
{
for(j = 0;j<625;j++);
}
}
老师的代码
while{
/*消隐*/
P0 = 0xFF;
P2 = P2 & 0x1F | 0xC0;
P2 &= 0x1F;
/*位选*/
P0 = 0x01;
P2 = P2 & 0x1F | 0xC0;
P2 &= 0x1F;
/*消隐*/
P0 = 0xFF;
P2 = P2 & 0x1F | 0xE0;
P2 &= 0x1F;
/*段选*/
//例:1010 0100
P0 = 0xA4;
P2 = P2 & 0x1F | 0xE0;
P2 &= 0x1F;
}
显示‘2001’
#include <stdio.h>
#include <STC15F2K60S2.h>
//显示2001
int i;
int main(){
while (1) { //死循环,防止程序跑飞;
for (i = 0; i < 4; i++) { //位选前四位;
/*位选*/
P0 = 1 << i; //选择com接口;
P2 = P2 & 0x1F | 0xC0; //选择Y6;
P2 &= 0x1F; //锁存;
/*消隐*/
P0 = 0xFF;
P2 |= 0xE0;
P2 &= 0x1F;
/*位选*/
switch (i) {
case 0:P0 = 0xA4; break; //使得第一个数码管显示‘2’;
case 1:P0 = 0x80; break;
case 2:P0 = 0x80; break;
case 3:P0 = 0xF9; break;
}
P2 |= 0xE0; //选择Y7
P2 &= 0x1F; //锁存
/*消隐*/
P0 = 0xFF;
P2 |= 0xC0;
P2 &= 0x1F;
}
}
}
问题
数码管亮度不一致,原因是最后一个的高电平时长远大于前面(执行完最后一个时就去执行别的了,而不是循环执行点亮)。
解决方法:数码管显示写在定时器里,使得每个数码管的占空比(高电平所占比率)一致。
注意
1、消隐不能只写一个 P0 = 0xFF,要写连通 Y 区的,把消隐导入进去。
/*消隐*/
P2 = P2 & 0x1F | 0xE0;
P0 = 0xFF;
P2 &= 0x1F;
2、简写的与或非要和等号靠在一起;
P2 &= 0xC0; //正确
P2 & = 0xC0; //错误
P2& = 0xC0; //错误
3、尽量先写 P2 再写 P0.