led 实验
-
本次实验是点亮 led ,我在实验中添加了一个枚举类型,用来存放 573 的值。可以提高代码可读性,减少错误。
//bug: 数码管段选和位选一定要写正确,当无法使用 573 控制器件时,应该检查该枚举类型 enum { //清除 Y4C Y5C Y6C Y7C clc = 0x1f, led = 0x80, rmb = 0xa0, com = 0xc0, seg = 0xe0 };
-
其次我添加了一个初始化程序,关闭蜂鸣器、继电器、led 和数码管。数码管是共阳极。数码管位选是高电平选中。
void init(void) { P2 = ((P2&clc) | rmb); //关闭数码管、蜂鸣器、继电器 P0 = 0x00; P2 = ((P2&clc) | led); P0 = 0xff; P2 = ((P2&clc) | seg); P0 = 0xff; P2 = ((P2&clc) | com); P0 = 0xff; }
中断实验
中断初始化函数应该考虑以下内容:
-
首先确定使用哪一个定时器;
-
通过 AUXR 配置各个定时器速率,[6:7] 分别为 T1 和 T0 速率控制位,[3] 为 T2 速率控制位;如果使用 T2 定时器,则需要在这里打开;
-
配置定时器工作模式,配置定时器初值;
-
打开定时器,打开定时器中断,打开总中断。
//exp for T1
void init_T1(void)
{
TMOD = 0x00; //选择模式0,16位重装载
AUXR &= 0x3f; //选择 12M 工作频率
TL1 = 0xF0;
TH1 = 0xD8;
TR1 = 1; //打开定时器
ET1 = 1; //打开定时器中断
EA = 1; //打开总中断
}
中断向量表
usart 实验
重新复习了串口初始化以及串口中断。
因为比赛使用的是 stc 下载器,它能够生成延时函数。但如果时间过长,则会使用NOP
。这个操作的定义在头文件 intrins.h
中。所以要额外包含这个头文件。
//exp
#include "intrins.h"
void Delay100ms() //@12.000MHz
{
unsigned char i, j, k;
_nop_();
_nop_();
i = 5;
j = 144;
k = 71;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
seg 实验
-
写 seg 花了好长时间,莫名其妙的问题。但也发现一些应该注意的点。由于 573 的转换需要时间,而单片机的运行速度很快,这就导致在数码管刷新过程中,573 还未锁存完成,P2 和 P0已经改变数值,导致其他数码管产生残影。为避免此情况产生,需要进行延时。每一位数码管应该留至少300us的等待时间,才能够保证正常显示。
-
数码管段码计算方式。如下图所示,数码管的八段代表八位,DP 为最高位,A 为最低位。比赛所使用的是共阳数码管,如果要显示数字 1,则应该 B 和 C 亮,其他位灭。所代表的 hex 为:
11111001-0xf9
。
-
经过实验和测试,我采用 1ms 中断扫描来显示数码管,一次中断更新一位数码管。这样编写代码不仅可以有效避免残影的产生,而且方便快捷。代码如下所示:
unsigned char code SEG[] = { 0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90, 0xff}; //段码表 unsigned char buf[] = { 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a}; //数码管缓冲区,储存所要显示的数值 unsigned char slt[] = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80}; //数码管位选数组,便于使用引索位选 //中断内调用此函数 void seg_scan() { static uchar i = 0; //声明为静态变量,作为引索值 P2 = ((P2&clc) | seg); P0 = SEG[buf[i]]; P2 = ((P2&clc) | com); P0 = slt[i]; P2 &= clc; i++; if (i >= 8) //满八次清零 i = 0; }
DS18B20
one-wire
一根数据线,当空闲时,总线为高;当数据来临,总线被主机或从机拉低,开始数据传输。以下是单总线传输协议。
应答协议(初始化)
主机拉低至少 480us 用来产生复位脉冲,之后释放总线。一段时间后从机会产生应答信号,拉低总线。此时如果程序检测到应答信号,则可证明从机已经准备好。
注意:程序检测应答信号时,一定要延时一段时间之后再进行检测。
读协议
主机发出读数据命令后,拉低总线,开启读时序。等待 1us 后从机发出数据,并保持 15us。结束后释放总线。
所有读时序应至少 60us。
写协议
写时序开始于主机拉低总线。
写 1:拉低总线,15us 后释放。
写 0:拉低总线,持续 60us。
所有写时序至少 60us。
函数编写
比赛提供了底层驱动文件,我们可以直接使用。下面我简单地介绍一下官方给的驱动文件。
我这里为了主程序的简洁,所以将编写的读温度函数放在了 .c 文件中。
onewire.h
在这个文件中,已经添加了一个读温度函数声明。但我们需要自己编写该函数。
#ifndef __ONEWIRE_H
#define __ONEWIRE_H
unsigned char rd_temperature(void); //; ;
#endif
onewire.c
//延时函数,由于我们使用的是 15 芯片,速度比 51 快 12 倍,所以我们要将该延时增长为
//12t
void Delay_OneWire(unsigned int t)
{
t = 12t;
while(t--);
}
读取温度流程:
-
先调用应答函数,调用写数据函数,写入
0xcc
,因为单总线上只有一个18b20,所以可以跳过寻址。 -
写入
0x44
,开始进行温度转换。 -
再次调用应答函数,写入
0xcc
,写入0xbe
,将数据输出。 -
数据低位先出,调用
ReadByte
先读取低八位,再读取高八位。18b20 的数据都是低位先出,编写
ReadByte()
函数时,应该注意数据方向。
unsigned char rd_temperature()
{
bit s;
uchar temp;
uchar th, tl;
uint tem;
if (!(s = init_ds18b20()))
return temp;
Delay500us();
Write_DS18B20(0xcc);
Delay1us();
Write_DS18B20(0x44);
Delay100ms();
if (!(s = init_ds18b20()))
return temp;
Delay500us();
Write_DS18B20(0xcc);
Delay1us();
Write_DS18B20(0xbe);
tl = Read_DS18B20();
th = Read_DS18B20();
tem = (th << 8) | tl;
tem >>= 4;
temp = (uchar)tem;
return temp;
}
At24c02
IIC
该通信协议使用两根线,SDA(数据)、SCL(时钟)。
时序格式
IIC 在数据传输过程中,时钟线为高电平时,数据不允许变化;时钟线为低电平时,数据的高低才允许变化。
起始结束信号
起始信号:当时钟线为高电平时,数据线由高电平改变为低电平代表起始信号。
结束信号:当时钟线为高电平时,数据线由低电平改变为高电平代表结束信号。
数据格式
数据格式为 MSB 。
send: | S | addr | 0 | A | dat | A | stop |
---|---|---|---|---|---|---|---|
发送 | 起始信号 | 器件地址 | 写标志 | 应答 | 数据 | 应答 | 结束 |
rec: | S | addr | 0 | byte_addr | A | S | addr | 1 | dat | NA | stop |
---|---|---|---|---|---|---|---|---|---|---|---|
接收 | 起始信号 | 器件地址 | 写标志 | 字节地址 | 应答 | 起始信号 | 字节地址 | 读标志 | 数据 | 非应答 | 结束 |
发送器件地址后,紧跟着一个读写标志,0 为写;1 为读。
应答:主机发出一个字节信号后被从机接收,从机发出一个低电平信号。
At24c02
A2 A1 A0 三个引脚选择器件地址,由外部控制;WP 为写保护,0 正常,1 只读。
读写方式
字节写入:在一次数据帧中只访问一个单元。数据帧格式如上表格所示。
页写入:在一个数据周期内,连续访问一页的内容。此种方式如果操作不当,字节地址会重新返回页首地址,产生“上卷”现象,导致数据写入失败。
当前地址读:发送器件地址后,直接读取数据。
选择读:数据帧格式如上表格所示。
程序设计
使用官方给的驱动文件。自己编写读 / 取函数。
更新:在延时内不用再开启中断,如果把中断打开,则会影响数据的接收。
//延时函数,为了减少中断的影响,在延时期间打开中断
void IIC_Delay(unsigned char i)
{
do{
_nop_();}
while(i--);
}
// add:字节地址;dat:数据
void At24c02Write(unsigned char add, unsigned char dat)
{
EA = 0; //关闭中断,防止中断影响传输过程
IIC_Start();
IIC_SendByte(SlaveAddrW);
IIC_WaitAck(); //等待应答不能少
IIC_SendByte(add);
IIC_WaitAck();
IIC_SendByte(dat);
IIC_WaitAck();
IIC_Stop();
EA = 1; //重新打开中断
}
unsigned char AT24c02Read(unsigned char add)
{
unsigned char dat;
EA = 0;
IIC_Start();
IIC_SendByte(SlaveAddrW);
IIC_WaitAck();
IIC_SendByte(add);
IIC_WaitAck();
IIC_Start();
IIC_SendByte(SlaveAddrR);
IIC_WaitAck();
dat = IIC_RecByte();
IIC_SendAck(1);
IIC_Stop();
EA = 1;
return dat;
}
PCF8591
这款芯片是 AD、DA 一体的,通过写入控制字节,来选择通道和输出数据。使用 IIC 通信协议。
地址字节
地址的高四位固定为 1001,A2、A1、 A0 为器件的地址,再跟一位读写控制字。
控制字节
0 | X | X | X | 0 | X | X | X |
---|---|---|---|---|---|---|---|
0 | DA 输出控制;写1使能 | 0 | 0 | 0 | 0 | 通道 | 通道 |
读写方式
一共三个字节,先地址,再控制,最后是 DA 数据。
读通道
我使用两个函数,一个是初始化函数,将通道配置好;另一个是读函数,写入地址后,直接读取数值。
DA 输出
DA 输出只能按顺序写入字节,先写地址-控制字打开 DA(这里要注意通道值,最好不要更改通道)-写入 DA 参数。
IIC 的读写不能一块调用,之间应该延时一段时间。
DS1302
信号传输格式
控制字节
[7] : 必须为1,如果为0,则 1302 不工作。
[6] : 选择是写日期还是写 RAM。
1302 拥有 31x8 的 RAM 空间
[1:5] : 指定了要操作的寄存器
[0] : 选择读写模式
数据字节
CH:停止位,如果为1,时钟停止计时;为0,开始计时。
写入格式为 BCD 码,不能使用十进制。
按键
比赛会使用两种操作,独立按键和矩阵按键。这里默认大家都知道按键原理。
独立按键
此种很简单,当按键按下时,管脚检测到低电平,代表按键按下。但我们在实际使用中,按键按下会发生抖动,导致程序多次检测到按键。为了避免此种情况,我们应该让程序在电平稳定的情况下测量,即消抖。
消抖
一种我们使用延时消抖。经过实践,按键抖动一般有 10ms 左右,所以我们只需在检测到低电平后延时 10ms 就可以跳过抖动。此时我们再次检测按键就能得出正确结果。
但延时消抖会让程序死等,也会浪费资源,所以我们采用采样的方式消抖。
在中断服务函数内,对按键采样,如果定时 1ms ,则进一次中断,采一次样。如果采样 8 次,发现这 8 次都是低电平,那我们就可以认为按键已经按下。
这种消抖方式在中断内执行,并不会导致程序死等,避免了资源浪费。
实现方式
先上代码
void key_scan();
void key_drive();
void key_action(uchar keycode);
uchar KeySta[] = {
1, 1, 1, 1};
uchar KeyMap[] = {
0x01 , 0x02 , 0x03, 0x04}; //这是按键对应的键值,可以方便我们查看按键,也可和标准接轨。可以自己设定
uchar Backup[] = {
1, 1, 1, 1}; //保存上一次的键值,防止多次触发按键
/** * 这个函数在主循环内调用,持续判断是否有按键按下 **/
void key_drive()
{
uchar i;
for (i=0; i<4; i++)
{
if (KeySta[i] != 0)
{
if (Backup[i] != 1)
{
key_aciton(KeyMap[i]); //触发按键动作
}
Backup[i] = KeySta[i]; //保存状态
}
}
}
void key_action(uchar keycode)
{
// 编写按键动作
}
/** * 这个函数在中断内调用,推荐时间 1ms **/
void key_scan()
{
static uchar KeyBuf[] = {
0xff, 0xff, 0xff, 0xff}; //采样缓冲区
uchar i;
KeyBuf[0] = (KeyBuf[0] << 1) | P30;
KeyBuf[1] = (KeyBuf[1] << 1) | P31;
KeyBuf[2] = (KeyBuf[2] << 1) | P32;
KeyBuf[3] = (KeyBuf[3] << 1) | P33; //四个按键的采样
for (i=0; i<4; i++)
{
if ((KeyBuf[i] & 0xff) == 0xff)
{
KeySta[i] = 1; //8 次全为1,则认为按键没有按下
}
else if ((KeyBuf[i] & 0xff) == 0x00)
{
KeySta[i] = 0; //8 次全为0,则认为按键按下
}
}
}
矩阵按键
矩阵按键比独立按键复杂许多,但思想是一模一样的。只是多出了几个按键。
上代码
void key_drive();
void key_scan();
void key_action(uchar);
uchar KeySta[4][4] = {
//4x4 的按键,所以使用 2 维数组。其实很简单,就是四个一维数组
{
1, 1, 1, 1},
{
1, 1, 1, 1},
{
1, 1, 1, 1},
{
1, 1, 1, 1}
};
uchar code KeyMap[4][4] = {
//对应板子上的值
{
0x07, 0x11, 0x15, 0x19},
{
0x06, 0x10, 0x14, 0x18},
{
0x05, 0x09, 0x13, 0x17},
{
0x04, 0x08, 0x12, 0x16}
};
void key_drive()
{
static uchar back_up[4][4] = {
{
1, 1, 1, 1},
{
1, 1, 1, 1},
{
1, 1, 1, 1},
{
1, 1, 1, 1}
};
uchar i, j;
for (i=0; i<4; i++) //遍历所有二维数组
{
for (j=0; j<4; j++)
{
if (KeySta[i][j] != back_up[i][j])
{
if (back_up[i][j] != 0)
{
key_action(KeyMap[i][j]);
}
back_up[i][j] = KeySta[i][j];
}
}
}
}
void key_action(uchar code)
{
}
void key_scan()
{
static uchar key_buf[4][4] = {
{
0xff, 0xff, 0xff, 0xff},
{
0xff, 0xff, 0xff, 0xff},
{
0xff, 0xff, 0xff, 0xff},
{
0xff, 0xff, 0xff, 0xff}
};
static uchar key_index = 0; //引索,代表本次扫描的行数
uchar j;
key_buf[key_index][0] = (key_buf[key_index][0] << 1) | P44; //采样
key_buf[key_index][1] = (key_buf[key_index][1] << 1) | P42;
key_buf[key_index][2] = (key_buf[key_index][2] << 1) | P35;
key_buf[key_index][3] = (key_buf[key_index][3] << 1) | P34;
for (j=0; j<4; j++)
{
if ((key_buf[key_index][j] & 0x0f) == 0x0f)
{
KeySta[key_index][j] = 1;
}
else if ((key_buf[key_index][j] & 0x0f) == 0x00)
{
KeySta[key_index][j] = 0;
}
}
key_index++;
key_index &= 0x03; //满 4 清零
switch (key_index) //拉低下一次要扫描的行数
{
case 0: P33 = 1; P30 = 0; break;
case 1: P30 = 1; P31 = 0; break;
case 2: P31 = 1; P32 = 0; break;
case 3: P32 = 1; P33 = 0; break;
}
}
文章评论