I/O扩展篇(基于74HC164/74HC165)
在我们的单片机应用系统中,常常会遇到I/O口不够的情况。譬如说接有外部RAM而且要求有16个以上的按键,8位数码管以上的显示。而且还不包括其它的外围器件。这时整个系统的I/O资源就很吃紧了。系统的扩展性也不好。这时我们就需要考虑对单片机的I/O进行扩展了。
虽然专门的I/O扩展芯片市场上也有不少,但对于我们一般的应用,没有必要整的那么复杂。用一些简单的移位寄存器芯片一样可以实现我们的目标。下面我们首先来认识一下74HC164这款芯片。这款芯片的作用是把串行输入的数据并行输出。注意,它没有锁存功能,在允许输出的情况下,每一个时钟的上升沿,数据依次从最低位移向最高位。因此,在做数码管的输出显示的时候会出现拖影的想象,在设计此电路时要注意考虑此情况。
下面是它的引脚图。A1,A2是数据输入端,一般情况下两者连在一起,作为串行数据的输入端。Qa----Qh j就是并行数据的输出端了。CLOCK 和RESET分别为时钟和复位端
下面我们再看看它的真值表,有了真值表我们才知道如何正确的去编写程序去驱动它(其它复杂的器件还需要对照时序图编写相应的驱动程序)
下面我以级联的8块74HC164驱动8位共阴的数码管为例来阐述它的用途。当然它的用途并不仅仅在于此。你可以发挥你的聪明才智去应用它到你的设计中。
以上的连接中Reset脚要全部接高电平。所有的Clock引脚都要连接在一块。第一块74HC164的AB引脚接在一块作为串行数据的输入端。第二块74HC164的AB引脚接在第一块74HC164并行数据输出端的H脚上。后面的接法依照第二块的接法依次级联下去。
接好后共引出四根引线。其中电源两根。一根时钟线。一根串行数据输入线。怎么样,节省了不少IO口吧~~
下面看看如何写程序去驱动它。(编译器keil Uv3)
先看看下面的引脚连接及相关宏定义
sbit io_74hc164_SCK = P3^7 ;
sbit io_74hc164_SDA = P3^6 ;
#define IO_74HC164_SCK_HIGH io_74hc164_SCK = 1 ;
#define IO_74HC164_SCK_LOW io_74hc164_SCK = 0 ;
#define IO_74HC164_SDA_INPUT io_74hc164_SDA
下面是数码管的段码表可以根据不同的连接顺序去修改。
/***********************************************************
a -- 4 b -- 5 c -- 6 d -- 2
e -- 0 f -- 1 g -- 3 dp -- 7
***********************************************************/
uint8 code DisplayTable[]=
{
0x77,0x60,0x3D,0x7C,0x6A,0x5E,0x5F,0x70,0x7F,0x7E,0x7B,0x4F,0x17,0x6D,0x1F,0x1B,0x08/*0 1 2 3 4 5 6 7 8 9 a b c d e f - */
};
void v_74hc164WriteData_f( uint8 Dat ) //向74HC164写一个字节的内容
{ //即可并行输出该字节
uint8 i = 0 ;
uint8 SendData = Dat ;
for( i = 8 ; i > 0 ; i-- )
{
IO_74HC164_SCK_LOW
SendData <<= 1 ;
IO_74HC164_SDA_INPUT = CY ;
IO_74HC164_SCK_HIGH
}
}
void v_HexToBcd_f( uint8 *P, uint16 Dat ) //BCD码的转化
{
uint8 i = 0 ;
uint8 Temp ;
if( Dat >= 40000 ) { i = 4 ; Dat -= 40000 ; }
if( Dat >= 20000 ) { i += 2 ; Dat -= 20000 ; }
if( Dat >= 10000 ) { i += 1 ; Dat -= 10000 ; }
*P++ = i ;
i = 0 ;
if( Dat >= 8000 ) { i = 8 ; Dat -= 8000 ; }
if( Dat >= 4000 ) { i += 4 ; Dat -= 4000 ; }
if( Dat >= 2000 ) { i += 2 ; Dat -= 2000 ; }
if( Dat >= 1000 ) { i += 1 ; Dat -= 1000 ; }
*P++ = i ;
i = 0 ;
if( Dat >= 800 ) { i = 8 ; Dat -= 800 ; }
if( Dat >= 400 ) { i += 4 ; Dat -= 400 ; }
if( Dat >= 200 ) { i += 2 ; Dat -= 200 ; }
Temp = Dat ; //这里换成8位数据,是为了加快速度
if( Temp >= 100 ) { i += 1 ; Temp -= 100 ; }
*P++ = i ;
i = 0 ;
if( Temp >= 80 ) { i = 8 ; Temp -= 80 ; }
if( Temp >= 40 ) { i += 4 ; Temp -= 40 ; }
if( Temp >= 20 ) { i += 2 ; Temp -= 20 ; }
if( Temp >= 10 ) { i += 1 ; Temp -= 10 ; }
*P++ = i ;
*P = Temp ;
}
/**************************************************************************
* Function: void v_74hc164DisplayNumber_f( uint8 data *Seg, uint8 Dot, int16 Dat ) *
* Description: 在8位数码管数值以及两位自定义字符 *
* * *
* Parameter: *Seg : 指向存放自定义字符数据的地址 *
* Dot : 小数点相对数值的显示位置(取值范围1~5,当取0 或者大于5的数值时,小数点不显示) *
* Dat : 显示数据(有符号整型数据,取值范围-32768~32767) *
**************************************************************************/
void v_74hc164DisplayNumber_f( uint8 data *Seg, uint8 Dot, int16 Dat )
{
bit zf = 1, OverWrite = 1, zf_lock = 1 ;
uint8 i , j , k = 4 ;
uint8 Buffer[5] ;
if ( Dat < 0 )
{
zf = 0 ;
Dat = ABS( Dat ) ; //如果是负数,则取其绝对值,并将负值标志位清0
}
v_HexToBcd_f( Buffer, Dat ) ; //将数据每个位拆分,放在数组中最高位放在数组的第//一个成员
for( i = 5 ; i >= 2 ; i-- ) //判断数据的位数(如1234,则位数为4)
{
if( Buffer[ 5 - i ] > 0 ) break ; //判断出最高位不为0即可
}
if( ( Dot >= i ) && Dot < 6 ) i = Dot ; //如果小数点打在数字前面,则该数字前面添0.( 数//字12,小数点打在第四位,则合理的显示应该为0.0012)
j = 5 - i ;
for( ; i >= 1 ; i-- ) //显示数值
{
if( Dot == ( 5 - k ) ) //如果该位有小数,则显示应该加上一个'.'
v_74hc164WriteData_f( DisplayTable[ Buffer[ k ] ] | 0x80 ) ;
else
v_74hc164WriteData_f( DisplayTable[ Buffer[ k ] ] ) ;
k-- ;
}
if( zf_lock ) //判断正负,如果为负值则显示'-'号,否则显示空
{
if( ( zf == 0 ) )
{
v_74hc164WriteData_f( 0x08 ) ;
}
else
{
v_74hc164WriteData_f( 0x00 ) ;
}
zf_lock = 0 ;
}
for( ; j > 0 ; j-- ) //多余的位显示空
{
v_74hc164WriteData_f( 0x00 ) ;
}
v_74hc164WriteData_f( Seg[ 1 ] ) ; //显示第一个自定义编码的字符
v_74hc164WriteData_f( Seg[ 0 ] ) ; //显示第二个自定义编码的字符
}
我们要想显示数值,直接调用这个函数v_74hc164DisplayNumber_f( uint8 data *Seg, uint8 Dot, int16 Dat )就可以了。看看显示效果。
其中PC是我们显示的自定义字形。小数点的位置是用程序固定打在某处的。以满足某些情况下的特殊要求。
仁者见仁,智者见智。相信你弄懂了它的用法,就可以把它灵活的应用到你设计中去了。
下面让我们来认识另外一种芯片。74HC165。看它的名字就知道它和74HC164有那么一点点关系了。我们之前已经知道了74HC164是串行输入并行输出的移位寄存器。而74HC165恰好相反。它是并行输入,串行输出。用来作单片机系统的输入部分的扩展是一个不错的选择。和上面一样。我们在使用之前需要对这个芯片有一个明确的了解。了解一个芯片最好的办法就是看它的DATASHEET了。有一份DATASHEET在手,自己先看看。对它的引脚及功能特性要了解。如果觉得不够的话,还可以上网去搜索一下用过这款芯片的人的使用经验。呵呵,那样可以使你少走不少弯路哦。
上面这幅图就是74HC165的引脚图了。其中A~H就是8位并行数据的输入端。Qh 和/Qh 是串行数据的输出端。为什么有两个输出端呢。它们是互补输出的。也就是说一个输出1时另外一个输出的是0,以满足某些应用场合的特殊要求。SER是串行数据的输入端。这其实为我们的将多块芯片级连起来创造了很好的条件。而事实上最后我们确实也是通过这个引脚将多块74HC165级联起来使用。CLK是时钟端。SH/LD是移位和锁存并行数据端。具体的介绍大家可以去看DATASHEET。
上面这幅图就是功能描述了。相信大家对这段英文都不是很陌生。如果你看不懂的话可以动手去查字典。想学电子的人不会看英文的资料可不行。这里希望大家不要再抱有什么幻想。基本上看原文的DATASHEET是最好的选择。如果你懒,可以去看别人的翻译版本。但是如果没有翻译的版本呢?
OK,下面依旧是以一个实例来说明它的用法。我们用两块74HC165级联起来组成的有16个按键的键盘为例来讲解。
下面是电路连接图
图中J2,J3是两个上拉电阻(8位一体的排阻,阻值取4.7k左右就可以了)。
下面来让我们一起去驱动它吧。
//下面是引脚的连接以及相关必要的宏定义
sbit io_74hc165_SH_LD = P2^0 ;
sbit io_74hc165_CLK = P2^1 ;
sbit io_74hc165_SDA = P2^2 ;
#define MAX_NUM_74HC165 2
#define NOKEY 0x00
#define KEY_WAIT 0
#define KEY_PRESS 1
#define KEY_CONFIRM 2
#define KEY_WAIT_REALSE 3
static u8_Read74hc165_f( void )
{
uint8 i, j ;
uint8 KeyAddress[ MAX_NUM_74HC165 ] ;
uint8 ReadReturn ;
io_74hc165_SH_LD = 0 ; //锁存并行数据开始
io_74hc165_SDA = 1 ; //准备读串行数据(也起到延时作用)
io_74hc165_SH_LD = 1 ; //锁存并行数据结束
for( j = 0 ; j < MAX_NUM_74HC165 ; j++ )
{
for( i = 8 ; i >= 1 ; i-- )
{
io_74hc165_CLK = 0 ; //时钟拉低
if( io_74hc165_SDA == 0 )break ; //有键按下,数据为1
io_74hc165_CLK = 1 ; //时钟拉高
}
KeyAddress[ j ] = i ; //有键压下,则i的取值在1~8之间,无键压下,i = 0
}
for( j = 0 ; j < MAX_NUM_74HC165 ; j++ )
{
if( KeyAddress[ j ] == 0 ) ReadReturn = 0x00 ;
else
{
ReadReturn = KeyAddress[ j ] + j * 8 ;
break ;
}
}
return ReadReturn ;
}
下面的这个函数就是读键盘的函数了。
uint8 u8_ReadKeyboard74hc165_f( void )
{
static uint8 KeyState = KEY_WAIT ;
uint8 KeyTemp = NOKEY, KeyValue = NOKEY ;
KeyTemp = u8_Read74hc165_f() ;
switch( KeyState )
{
case KEY_WAIT : if( KeyTemp == NOKEY ) KeyState = KEY_WAIT ;
else KeyState = KEY_PRESS ; break ;
case KEY_PRESS : if( KeyTemp == NOKEY ) KeyState = KEY_WAIT ;
else KeyState = KEY_CONFIRM ; break ;
case KEY_CONFIRM : if( KeyTemp == NOKEY ) KeyState = KEY_WAIT ;
else
{
KeyState = KEY_WAIT_REALSE ;
KeyValue = KeyTemp ;
} break ;
case KEY_WAIT_REALSE :if( KeyTemp != NOKEY ) KeyState = KEY_WAIT_REALSE ;
else
{
KeyState = KEY_WAIT ;
}break ;
default : break ;
}
return KeyValue ;
}
我们在主函数中调用u8_ReadKeyboard74hc165_f( ) 就可以得到键值了。
上图我用74HC164级联的数码管来显示74HC165键盘的键值的情况。当按第二个键时显示的是2.
到此单片机的IO口的扩展告一段落。希望这两个小例子能够给你一点启发。能够给你在以后的学习及设计中带来一点灵感
虽然专门的I/O扩展芯片市场上也有不少,但对于我们一般的应用,没有必要整的那么复杂。用一些简单的移位寄存器芯片一样可以实现我们的目标。下面我们首先来认识一下74HC164这款芯片。这款芯片的作用是把串行输入的数据并行输出。注意,它没有锁存功能,在允许输出的情况下,每一个时钟的上升沿,数据依次从最低位移向最高位。因此,在做数码管的输出显示的时候会出现拖影的想象,在设计此电路时要注意考虑此情况。
下面是它的引脚图。A1,A2是数据输入端,一般情况下两者连在一起,作为串行数据的输入端。Qa----Qh j就是并行数据的输出端了。CLOCK 和RESET分别为时钟和复位端
下面我们再看看它的真值表,有了真值表我们才知道如何正确的去编写程序去驱动它(其它复杂的器件还需要对照时序图编写相应的驱动程序)
下面我以级联的8块74HC164驱动8位共阴的数码管为例来阐述它的用途。当然它的用途并不仅仅在于此。你可以发挥你的聪明才智去应用它到你的设计中。
以上的连接中Reset脚要全部接高电平。所有的Clock引脚都要连接在一块。第一块74HC164的AB引脚接在一块作为串行数据的输入端。第二块74HC164的AB引脚接在第一块74HC164并行数据输出端的H脚上。后面的接法依照第二块的接法依次级联下去。
接好后共引出四根引线。其中电源两根。一根时钟线。一根串行数据输入线。怎么样,节省了不少IO口吧~~
下面看看如何写程序去驱动它。(编译器keil Uv3)
先看看下面的引脚连接及相关宏定义
sbit io_74hc164_SCK = P3^7 ;
sbit io_74hc164_SDA = P3^6 ;
#define IO_74HC164_SCK_HIGH io_74hc164_SCK = 1 ;
#define IO_74HC164_SCK_LOW io_74hc164_SCK = 0 ;
#define IO_74HC164_SDA_INPUT io_74hc164_SDA
下面是数码管的段码表可以根据不同的连接顺序去修改。
/***********************************************************
a -- 4 b -- 5 c -- 6 d -- 2
e -- 0 f -- 1 g -- 3 dp -- 7
***********************************************************/
uint8 code DisplayTable[]=
{
0x77,0x60,0x3D,0x7C,0x6A,0x5E,0x5F,0x70,0x7F,0x7E,0x7B,0x4F,0x17,0x6D,0x1F,0x1B,0x08/*0 1 2 3 4 5 6 7 8 9 a b c d e f - */
};
void v_74hc164WriteData_f( uint8 Dat ) //向74HC164写一个字节的内容
{ //即可并行输出该字节
uint8 i = 0 ;
uint8 SendData = Dat ;
for( i = 8 ; i > 0 ; i-- )
{
IO_74HC164_SCK_LOW
SendData <<= 1 ;
IO_74HC164_SDA_INPUT = CY ;
IO_74HC164_SCK_HIGH
}
}
void v_HexToBcd_f( uint8 *P, uint16 Dat ) //BCD码的转化
{
uint8 i = 0 ;
uint8 Temp ;
if( Dat >= 40000 ) { i = 4 ; Dat -= 40000 ; }
if( Dat >= 20000 ) { i += 2 ; Dat -= 20000 ; }
if( Dat >= 10000 ) { i += 1 ; Dat -= 10000 ; }
*P++ = i ;
i = 0 ;
if( Dat >= 8000 ) { i = 8 ; Dat -= 8000 ; }
if( Dat >= 4000 ) { i += 4 ; Dat -= 4000 ; }
if( Dat >= 2000 ) { i += 2 ; Dat -= 2000 ; }
if( Dat >= 1000 ) { i += 1 ; Dat -= 1000 ; }
*P++ = i ;
i = 0 ;
if( Dat >= 800 ) { i = 8 ; Dat -= 800 ; }
if( Dat >= 400 ) { i += 4 ; Dat -= 400 ; }
if( Dat >= 200 ) { i += 2 ; Dat -= 200 ; }
Temp = Dat ; //这里换成8位数据,是为了加快速度
if( Temp >= 100 ) { i += 1 ; Temp -= 100 ; }
*P++ = i ;
i = 0 ;
if( Temp >= 80 ) { i = 8 ; Temp -= 80 ; }
if( Temp >= 40 ) { i += 4 ; Temp -= 40 ; }
if( Temp >= 20 ) { i += 2 ; Temp -= 20 ; }
if( Temp >= 10 ) { i += 1 ; Temp -= 10 ; }
*P++ = i ;
*P = Temp ;
}
/**************************************************************************
* Function: void v_74hc164DisplayNumber_f( uint8 data *Seg, uint8 Dot, int16 Dat ) *
* Description: 在8位数码管数值以及两位自定义字符 *
* * *
* Parameter: *Seg : 指向存放自定义字符数据的地址 *
* Dot : 小数点相对数值的显示位置(取值范围1~5,当取0 或者大于5的数值时,小数点不显示) *
* Dat : 显示数据(有符号整型数据,取值范围-32768~32767) *
**************************************************************************/
void v_74hc164DisplayNumber_f( uint8 data *Seg, uint8 Dot, int16 Dat )
{
bit zf = 1, OverWrite = 1, zf_lock = 1 ;
uint8 i , j , k = 4 ;
uint8 Buffer[5] ;
if ( Dat < 0 )
{
zf = 0 ;
Dat = ABS( Dat ) ; //如果是负数,则取其绝对值,并将负值标志位清0
}
v_HexToBcd_f( Buffer, Dat ) ; //将数据每个位拆分,放在数组中最高位放在数组的第//一个成员
for( i = 5 ; i >= 2 ; i-- ) //判断数据的位数(如1234,则位数为4)
{
if( Buffer[ 5 - i ] > 0 ) break ; //判断出最高位不为0即可
}
if( ( Dot >= i ) && Dot < 6 ) i = Dot ; //如果小数点打在数字前面,则该数字前面添0.( 数//字12,小数点打在第四位,则合理的显示应该为0.0012)
j = 5 - i ;
for( ; i >= 1 ; i-- ) //显示数值
{
if( Dot == ( 5 - k ) ) //如果该位有小数,则显示应该加上一个'.'
v_74hc164WriteData_f( DisplayTable[ Buffer[ k ] ] | 0x80 ) ;
else
v_74hc164WriteData_f( DisplayTable[ Buffer[ k ] ] ) ;
k-- ;
}
if( zf_lock ) //判断正负,如果为负值则显示'-'号,否则显示空
{
if( ( zf == 0 ) )
{
v_74hc164WriteData_f( 0x08 ) ;
}
else
{
v_74hc164WriteData_f( 0x00 ) ;
}
zf_lock = 0 ;
}
for( ; j > 0 ; j-- ) //多余的位显示空
{
v_74hc164WriteData_f( 0x00 ) ;
}
v_74hc164WriteData_f( Seg[ 1 ] ) ; //显示第一个自定义编码的字符
v_74hc164WriteData_f( Seg[ 0 ] ) ; //显示第二个自定义编码的字符
}
我们要想显示数值,直接调用这个函数v_74hc164DisplayNumber_f( uint8 data *Seg, uint8 Dot, int16 Dat )就可以了。看看显示效果。
其中PC是我们显示的自定义字形。小数点的位置是用程序固定打在某处的。以满足某些情况下的特殊要求。
仁者见仁,智者见智。相信你弄懂了它的用法,就可以把它灵活的应用到你设计中去了。
下面让我们来认识另外一种芯片。74HC165。看它的名字就知道它和74HC164有那么一点点关系了。我们之前已经知道了74HC164是串行输入并行输出的移位寄存器。而74HC165恰好相反。它是并行输入,串行输出。用来作单片机系统的输入部分的扩展是一个不错的选择。和上面一样。我们在使用之前需要对这个芯片有一个明确的了解。了解一个芯片最好的办法就是看它的DATASHEET了。有一份DATASHEET在手,自己先看看。对它的引脚及功能特性要了解。如果觉得不够的话,还可以上网去搜索一下用过这款芯片的人的使用经验。呵呵,那样可以使你少走不少弯路哦。
上面这幅图就是74HC165的引脚图了。其中A~H就是8位并行数据的输入端。Qh 和/Qh 是串行数据的输出端。为什么有两个输出端呢。它们是互补输出的。也就是说一个输出1时另外一个输出的是0,以满足某些应用场合的特殊要求。SER是串行数据的输入端。这其实为我们的将多块芯片级连起来创造了很好的条件。而事实上最后我们确实也是通过这个引脚将多块74HC165级联起来使用。CLK是时钟端。SH/LD是移位和锁存并行数据端。具体的介绍大家可以去看DATASHEET。
上面这幅图就是功能描述了。相信大家对这段英文都不是很陌生。如果你看不懂的话可以动手去查字典。想学电子的人不会看英文的资料可不行。这里希望大家不要再抱有什么幻想。基本上看原文的DATASHEET是最好的选择。如果你懒,可以去看别人的翻译版本。但是如果没有翻译的版本呢?
OK,下面依旧是以一个实例来说明它的用法。我们用两块74HC165级联起来组成的有16个按键的键盘为例来讲解。
下面是电路连接图
图中J2,J3是两个上拉电阻(8位一体的排阻,阻值取4.7k左右就可以了)。
下面来让我们一起去驱动它吧。
//下面是引脚的连接以及相关必要的宏定义
sbit io_74hc165_SH_LD = P2^0 ;
sbit io_74hc165_CLK = P2^1 ;
sbit io_74hc165_SDA = P2^2 ;
#define MAX_NUM_74HC165 2
#define NOKEY 0x00
#define KEY_WAIT 0
#define KEY_PRESS 1
#define KEY_CONFIRM 2
#define KEY_WAIT_REALSE 3
static u8_Read74hc165_f( void )
{
uint8 i, j ;
uint8 KeyAddress[ MAX_NUM_74HC165 ] ;
uint8 ReadReturn ;
io_74hc165_SH_LD = 0 ; //锁存并行数据开始
io_74hc165_SDA = 1 ; //准备读串行数据(也起到延时作用)
io_74hc165_SH_LD = 1 ; //锁存并行数据结束
for( j = 0 ; j < MAX_NUM_74HC165 ; j++ )
{
for( i = 8 ; i >= 1 ; i-- )
{
io_74hc165_CLK = 0 ; //时钟拉低
if( io_74hc165_SDA == 0 )break ; //有键按下,数据为1
io_74hc165_CLK = 1 ; //时钟拉高
}
KeyAddress[ j ] = i ; //有键压下,则i的取值在1~8之间,无键压下,i = 0
}
for( j = 0 ; j < MAX_NUM_74HC165 ; j++ )
{
if( KeyAddress[ j ] == 0 ) ReadReturn = 0x00 ;
else
{
ReadReturn = KeyAddress[ j ] + j * 8 ;
break ;
}
}
return ReadReturn ;
}
下面的这个函数就是读键盘的函数了。
uint8 u8_ReadKeyboard74hc165_f( void )
{
static uint8 KeyState = KEY_WAIT ;
uint8 KeyTemp = NOKEY, KeyValue = NOKEY ;
KeyTemp = u8_Read74hc165_f() ;
switch( KeyState )
{
case KEY_WAIT : if( KeyTemp == NOKEY ) KeyState = KEY_WAIT ;
else KeyState = KEY_PRESS ; break ;
case KEY_PRESS : if( KeyTemp == NOKEY ) KeyState = KEY_WAIT ;
else KeyState = KEY_CONFIRM ; break ;
case KEY_CONFIRM : if( KeyTemp == NOKEY ) KeyState = KEY_WAIT ;
else
{
KeyState = KEY_WAIT_REALSE ;
KeyValue = KeyTemp ;
} break ;
case KEY_WAIT_REALSE :if( KeyTemp != NOKEY ) KeyState = KEY_WAIT_REALSE ;
else
{
KeyState = KEY_WAIT ;
}break ;
default : break ;
}
return KeyValue ;
}
我们在主函数中调用u8_ReadKeyboard74hc165_f( ) 就可以得到键值了。
上图我用74HC164级联的数码管来显示74HC165键盘的键值的情况。当按第二个键时显示的是2.
到此单片机的IO口的扩展告一段落。希望这两个小例子能够给你一点启发。能够给你在以后的学习及设计中带来一点灵感