树莓派GPIO入门09-使用MAX7219芯片驱动8位数码管
前面几篇我们学习了如何利用一片或多片级联的74HC595芯片来驱动显示数码管的方法。最开始我们提出的两个问题：占用IO口过多做动态扫描时占用CPU资源太多导致显示不稳定第一个问题已经利用级联的74HC595解决了。（不管驱动几只数码管，只用3个IO即可）但第二个问题依然存在，树莓派仍然需要不停地访问IO口进行动态扫描。好，既然动态扫描占用CPU资源太多，那这活儿咱包出去让别人干不就行了？这就是本文主角MAX7219的特长，动态扫描这活儿MAX7219全包了，干的还比咱自己干得还好，一片MAX7219可最多支持显示8位8段数码管，还支持各种设置如亮度设置扫描位数设置等。而我们只用告诉它显示的内容就行了。最终效果视频演示http://v.youku.com/v_show/id_XMTI2MTU2OTc3Ng==.html?spm=a2hzp.8253869.0.0硬件MAX7219显示芯片 X 1（2到3元一个）共阴（共阳不可以）4位数码管 X 210k电阻一只10uF电容一只0.1uF电容一只原理说明注：下面这些图都是从MAX7219官方资料里截取出来的，官方资料文末提供下载先看一下MAX7219的引脚图： MAX7219一共有24根引脚，这里面有一些跟74HC595的引脚功能类似的引脚如下：MAX721974HC595功能DINDS串行输入SEG A-G,DPQ0-Q7数码管段数据输出CLKSHCP串行移位寄存器时钟LOADSTCP内部寄存器锁存时钟DOUTQ7S串行输出，级联多块芯片时使用可以看出，MAX7219跟74HC595一样也是通过DIN引脚串行输入数据，在CLK引脚的上升沿移入移位寄存器，在LOAD引脚的上升沿将位移寄存器的数据复制到内部的各种寄存器里。不同的是:74HC595的移位寄存器是8位的，而MAX7219的移位寄存器是16位的，每次串行输入数据需要连续输入16位数据。74HC595内部只有1个8位的锁存器，功能很简单，只是原样输出到8根输出引脚。而MAX7219内部有好几个不同功能的寄存器。后面我会逐个说明。我们再看一下官方文档里关于各引脚的功能说明： 上面提过的不再赘述，说一下MAX7219特有的引脚。DIG0-DIG7这8个引脚作为位选端，分别连到8位数码管的共阴极，MAX7219在做动态扫描时会依次向共阴极DIG0-DIG7输出低电平，所以MAX7219不能驱动共阳极的数码管。其实也不是绝对不能，将位选端和段选端逆接就可以了，不过这样做的话，有3个弊端：不能使用MAX7219内部的译码功能，需要自己编写数字的字形码。增加了代码的复杂度。需要以段单位来传输数据，而不能以位单位来传输数据。（即每次传输的是各位数码管的某一段的数据，而不是某位数码管各段的数据。第一次传所有数码管的A段数据，第二次传所有数码管的B段数据一直到8段数据全部传完）无疑进一步增加了代码的复杂度。无法限制扫描位数，只能所有8位数码管同时扫描。（变成了限制扫描段数，如可以只显示所有数码管的A-C段。。。）ISET通过一只电阻连上VCC，通过调整这只电阻的阻值可以调整通过这只电阻的电流I，而最后输出到数码管上的最大电流就是这个电流I的100倍。如果这只电阻太小，导致输出到数码管的电流过大会烧坏数码管，所以官方文档里推荐这只电阻最小应该设置为9.7K欧，我手头上只有10K的电阻也是一样，只要别低于这个阻值就可以了。根据这个原理我们也可以接一个可调电阻在ISET上，可以随时调整数码管亮度。这种调整亮度的方法是通过模拟信号调制。MAX7219也支持通过数字信号来设置亮度（有16级亮度可调）。MAX7219内部的各模块的示意图： 最下面是一个16位的移位寄存器D0-D15，跟74HC595一样，通过DIN引脚串行输入数据，在CLK引脚的上升沿移入移位寄存器。 D8-D11这4位数据用来指定内部寄存器地址。D0-D7是具体要传到寄存器里的数据。 上面的图看上去有点复杂，其实简单地说就是我们每次要串行传输16位数据进去，然后在LOAD的上升沿，根据D8-D11指定的寄存器地址，将D0-D7存放在指定的寄存器里。这样多次传输数据就可以给所有的寄存器设值了。 下面是MAX7219内部的寄存器一览：关断寄存器译码模式寄存器亮度寄存器扫描限值寄存器显示器测试寄存器每个寄存器都有自己特殊的功能，后面我们会逐一介绍。串行传输时的数据格式如下： 你可能注意到了这幅图的顺序跟模块示意图里移位寄存器的顺序是相反的。 D15-D0这个反向的顺序是你写代码时串行传输数据时的顺序，先传D15的数据，再传D14,D13...D0，这样传输完成后。实际的数据顺序就变成了D0,D1,D2...D15。为了便于编码，除了模块示意图是按实际顺序记载的，后面所有数据格式的示意图都是倒序记载的。各寄存器地址如下： 关断寄存器的数据格式： 译码模式寄存器的数据格式： 译码模式下的原码和译码的对应表： 非译码模式下的数据和数码管各段的对应表： 亮度寄存器的数据格式： 扫描限值寄存器的数据格式： 显示器测试寄存器的数据格式： 硬件连接模块1引脚模块2引脚MAX7219DIN树莓派GPIO13MAX7219LOAD树莓派GPIO26MAX7219CLK树莓派GPIO19MAX7219GNDX2树莓派GNDMAX7219V+树莓派5VMAX7219ISET树莓派5V(需串联一只10k电阻)MAX7219DIG0数码管1DIG1(引脚12)MAX7219DIG1数码管1DIG2(引脚9)MAX7219DIG2数码管1DIG3(引脚8)MAX7219DIG3数码管1DIG4(引脚6)MAX7219DIG4数码管2DIG1(引脚12)MAX7219DIG5数码管2DIG2(引脚9)MAX7219DIG6数码管2DIG3(引脚8)MAX7219DIG7数码管2DIG4(引脚6)MAX7219SEG DP数码管1/2DPMAX7219SEG G数码管1/2GMAX7219SEG F数码管1/2FMAX7219SEG E数码管1/2EMAX7219SEG D数码管1/2DMAX7219SEG C数码管1/2CMAX7219SEG B数码管1/2BMAX7219SEG A数码管1/2AMAX7219DOUT(级联用)(不使用)10/0.1uF电容正极MAX7219V+10/0.1uF电容负极MAX7219GND代码（Python）#!/usr/bin/env python # encoding: utf-8 import RPi.GPIO import time # 串行数据输入 DIN = 13 # 时钟脉冲信号——上升沿有效 LOAD = 26 # 打入信号————上升沿有效 CLK = 19 RPi.GPIO.setmode(RPi.GPIO.BCM) RPi.GPIO.setup(DIN, RPi.GPIO.OUT) RPi.GPIO.setup(LOAD, RPi.GPIO.OUT) RPi.GPIO.setup(CLK, RPi.GPIO.OUT) # 传输一个8位数 def setByte(byteData): for bit in range(0,8): # 传入的数字从高位到低位依次判断是否为1，若为1则设置高电平，否则设置低电平 # 判断的方法是先向左移位，把要判断的位移动到最高位然后跟0x80（1000 0000）相与， # 如果结果仍然是0x80（1000 0000）就表示最高位是1，否则最高位就是0 if ((byteData<<bit) & 0x80 == 0x80): setBitData(True) else: setBitData(False) # 译码模式设置 def setDecodeMode(mode = 4): RPi.GPIO.output(LOAD, False) # 指令寄存器地址设置：0xX9 # D15-D12:任意 # D11,10,9,8: 1,0,0,1 setByte(0x09) if (mode == 1) : # 所有数码管均不使用译码功能 # 指令代码: 0x00 setByte(0x00) elif (mode == 2) : # 只对DIG0号数码管进行译码，其他数码管不使用译码功能 # 指令代码: 0x01 setByte(0x01) elif (mode == 3) : # 对DIG0-3号数码管进行译码，其他数码管不使用译码功能 # 指令代码: 0x0F setByte(0x0f) elif (mode == 4) : # 数码管7－0全部采用译码模式 # 指令代码: 0xFF setByte(0xff) RPi.GPIO.output(LOAD, True) # 亮度设置 def setIntensity(mode = 8): RPi.GPIO.output(LOAD, False) # 指令寄存器地址设置：0xXA # D15-D12:任意 # D11,10,9,8: 1,0,1,0 setByte(0x0A) # 亮度从0到15共16个等级，指令的D3－D0就是数字0－15的二进制编码 # D7-D4:任意 setByte(mode) RPi.GPIO.output(LOAD, True) # 扫描显示位数设置(0-7) def setScanLimit(mode = 7): RPi.GPIO.output(LOAD, False) # 指令寄存器地址设置：0xXB # D15-D12:任意 # D11,10,9,8: 1,0,1,1 setByte(0x0B) # 扫描位数可设置0－7共8种选择，指令的D2－D0就是数字0－7的二进制编码 # D7-D3:任意 # D2-D0:0-7的3位二进制编码 setByte(mode) RPi.GPIO.output(LOAD, True) # 关断模式设置 # mode: 1: 正常运行模式 # mode: 0: 关断模式 def setShutdownMode(mode = 1): RPi.GPIO.output(LOAD, False) # 指令寄存器地址设置：0xXC # D15-D12:任意 # D11,10,9,8: 1,1,0,0 setByte(0x0C) # 关断模式可设置0－1共2种选择，设置D0即可 # D7-D1:任意 # D0 : 1: 正常运行模式 0: 关断模式 setByte(mode) RPi.GPIO.output(LOAD, True) # 测试模式设置 # mode=0: 正常运行模式 # mode=1: 测试模式(全亮模式) def setDisplayTestMode(mode = 0): RPi.GPIO.output(LOAD, False) # 指令寄存器地址设置：0xXF # D15-D12:任意 # D11,10,9,8: 1,1,1,1 setByte(0x0f) # 测试模式可设置0－1共2种选择，设置D0即可 # D7-D1:任意 # D0:0: 正常运行模式 1: 测试模式(全亮模式) setByte(mode) RPi.GPIO.output(LOAD, True) # 指定no(1-8)号数码管显示数字num(0-9)，第三个参数是显示不显示小数点（true/false） def showDigit(no, num, showDotPoint): RPi.GPIO.output(LOAD, False) # 设置指令寄存器地址：0xX1-0xX8 # 格式：D15-D12:任意（我们这里设置0） # D11-D8: 1-8的4位二进制编码：例：1（0,0,0,1） setByte(no) # 设置显示内容 # 格式：D7:显示小数点（1点亮） # D6-D4:任意（我们这里设置0） # D3-D0:数字0-9的4位二进制编码：例：2（0,0,1,0） if (showDotPoint): # 如果显示小数点则需要将数字的最高位（D7）设置为1(最高位跟1相或) setByte(num | 0x80) else: setByte(num) RPi.GPIO.output(LOAD, True) def setBitData(data): RPi.GPIO.output(CLK, False) RPi.GPIO.output(DIN, data) RPi.GPIO.output(CLK, True) try: # 测试代码，亮度使用最低亮度，显示从0开始递增的数字。 setShutdownMode() setDecodeMode() setIntensity(0) setScanLimit() setDisplayTestMode() n=0 while True: n=n+1 showDigit(8, int(n)%10, False) showDigit(7, int(n/10)%10, False) showDigit(6, int(n/100)%10, False) showDigit(5, int(n/1000)%10, False) showDigit(4, int(n/10000)%10, False) showDigit(3, int(n/100000)%10, False) showDigit(2, int(n/1000000)%10, False) showDigit(1, int(n/10000000)%10, False) time.sleep(0.01) except KeyboardInterrupt: pass # 清理GPIO口 RPi.GPIO.cleanup()资源下载源码MAX7219数据手册系列文章树莓派GPIO入门01-使用GPIO接口控制发光二极管闪烁树莓派GPIO入门02-GPIO控制LED亮度，制作呼吸灯效果树莓派GPIO入门03-GPIO控制RGB彩色LED灯树莓派GPIO入门04-使用按钮树莓派GPIO入门05-驱动数码管显示数字树莓派GPIO入门06-跟数字湿温度计DHT11通信树莓派GPIO入门07-利用声音传感器制作声控灯树莓派GPIO入门08-使用74HC595芯片驱动数码管（一）树莓派GPIO入门08-使用74HC595芯片驱动数码管（二）树莓派GPIO入门09-使用MAX7219芯片驱动8位数码管【当前文章】树莓派GPIO入门10-使用TLC5940芯片输出多路PWM树莓派GPIO入门11-驱动液晶屏幕（一）树莓派GPIO入门11-驱动液晶屏幕（二）
上一篇我们学习了如何利用74HC595这块显示驱动芯片来驱动1位8段数码管的方法。并逐一讲解了该芯片各引脚的功能。细心的童鞋可能注意到了，上次我们有一个Q7S引脚没有用到。这一篇我们就用这个引脚来级联两块74HC595芯片同时驱动8个8段数码管。在两块74HC595芯片的配合下，同样只需要3个GPIO口就能驱动八只8段数码管。而如果直接用GPIO驱动则需要至少16个GPIO口，节省了13个GPIO！而且按照本文的方法，你还可以级联更多的74HC595芯片，每多级联一片就能多驱动8只数码管，而需要的IO口仍然只有3个！妈妈再也不用担心我的IO口不够用了。最终效果视频演示http://v.youku.com/v_show/id_XMTI1NzE1MDQwOA==.html?spm=a2hzp.8253869.0.0硬件74HC595显示芯片 X 2共阳（或共阴）4位数码管 X 2原理说明我们已经知道，每次制造一次移位寄存器时钟引脚的上升沿，74HC595都会在这个上升沿将DS引脚上的数据存入内部的移位寄存器D0，同时D0原来的数据会顺移到D1，D1的数据位移到D2。。。D6的数据位移到D7。而D7的数据会被输出到引脚Q7S上，如果Q7S引脚没有被使用，那么这一位数据就被丢掉了。如果我们将Q7S引脚连接上另一块74HC595的DS引脚并保证两块芯片上的移位时钟的上升沿同时发生（将两块芯片的SHCP连在一起即可）的话，第一块74HC595的串行输出引脚Q7S就变成了第二块74HC595的串行输入数据源。Q7S上的数据会继续移位到第二块74HC595芯片的移位寄存器D0里（同样的，D0→D1→D2→...→D6→D7→Q7S）。16位数据全部这样串行传输完毕后，制造两块芯片的锁存时钟STCP的上升沿（跟SHCP一样，两块芯片的STCP连在一起即可）即可同时输出16位数据。利用上述级联原理加上前一篇学习的内容，我们可以这样静态驱动2只数码管：两只数码管的共阳(阴)极连接VCC(GND)，第一片595的Q0-Q7连到第一个数码管的A-G，Dp上，级联的第二片595的Q0-Q7连到第二只数码管的A-G,Dp上，然后用3根GPIO口驱动两片级联的74HC595芯片，连续串行输入两个数字的显示码共16位数据，然后同时制造锁存上升沿，将两个数字的显示码分别输出到两只数码管上即可。这样连接的话，N片级联的74HC595就能驱动N只数码管了。（咦，说好的驱动8位数码管的呢）上面的做法当然没问题，而且有一个优点是静态显示所以显示很稳定，有兴趣自己试着做一下。缺点是浪费芯片，价格倒不贵可是连线复杂，占用电路板面积太多。本文采用动态扫描（原理参见本系列第5篇）的方式来使用两块级联的74HC595芯片驱动8位数码管。原理也很简单，稍微调整一下硬件的连接方式和传输数据的方式即可。依然是两块74HC595级联，第一块595的输出引脚连到所有数码管的段引脚上（8只数码管的A-G,Dp引脚连在一起）,第二块595的输出引脚Q0-Q7分别连上8只数码管的共阳(阴)极上。第一块芯片的输出决定了数码管上显示什么数字。而8只数码管中哪一只会被点亮则取决于第二块芯片哪只数码管的共阳(阴)端是高(低)电平。由于在我的硬件连接里，级联的第二块74HC595是负责位选的，所以应该先发送8位位选信号，再发送8位段选信号，在连续发完了16位数据以后，第一块74HC595的移位寄存器里保存着8位的段选信息，而在第二块74HC595的移位寄存器里保存着8位的位选信息。由于两片芯片的STCP是连在同一个GPIO口的，这个时候如果制造一次STCP的上升沿，两块595的位移寄存器里的数据会同时保存到自己的锁存器里，由于我的使能端OE连在GND始终有效，锁存器里的数据就直接输出到了芯片的输出引脚上。第二块芯片的8根输出引脚连接的是8位数码管的共阳端。只有输出高电平的引脚连接的数码管才会被点亮。当然如果你喜欢，你也可以让8个数码管全部同时点亮，你只要让第二块595输出11111111就行了。不过显示的数字都是一样的。想显示不同的数字就要进行动态扫描，每次输出一个数字的显示码跟一个位选码，快速循环切换显示即可。跟上一篇一样，我也做了一个动画帮助你理解级联的过程。这个动画一开始已经是传输完前8位位选数据的状态了，动画里的位选数据是1000000，点亮第一个数码管，显示的内容由后8位段选数据决定，动画里是数字5。硬件连接模块1引脚模块2引脚74HC595_1Q0数码管1/2DP74HC595_1Q1数码管1/2G74HC595_1Q2数码管1/2F74HC595_1Q3数码管1/2E74HC595_1Q4数码管1/2D74HC595_1Q5数码管1/2C74HC595_1Q6数码管1/2B74HC595_1Q7数码管1/2A74HC595_1Q7S74HC595_2DS74HC595_2Q0数码管1DIG1的共阳极74HC595_2Q1数码管1DIG2的共阳极74HC595_2Q2数码管1DIG3的共阳极74HC595_2Q3数码管1DIG4的共阳极74HC595_2Q4数码管2DIG1的共阳极74HC595_2Q5数码管2DIG2的共阳极74HC595_2Q6数码管2DIG3的共阳极74HC595_2Q7数码管2DIG4的共阳极树莓派GPIO1374HC595_1DS树莓派GPIO1974HC595_1/2SHCP树莓派GPIO2674HC595_1/2SHTP树莓派VCC74HC595_1/2VCC,MR树莓派GND74HC595_1/2GND,OE代码（Python）#!/usr/bin/env python # encoding: utf-8 import RPi.GPIO import time # 串行数据输入引脚连接的GPIO口 DS = 13 # 移位寄存器时钟控制引脚连接的GPIO口——上升沿有效 SHCP = 19 # 数据锁存器时钟控制引脚连接的GPIO口——上升沿有效 STCP = 26 RPi.GPIO.setmode(RPi.GPIO.BCM) RPi.GPIO.setup(DS, RPi.GPIO.OUT) RPi.GPIO.setup(STCP, RPi.GPIO.OUT) RPi.GPIO.setup(SHCP, RPi.GPIO.OUT) RPi.GPIO.output(STCP, False) RPi.GPIO.output(SHCP, False) # 通过串行数据引脚向74HC595的传送一位数据 def setBitData(data): # 准备好要传送的数据 RPi.GPIO.output(DS, data) # 制造一次移位寄存器时钟引脚的上升沿（先拉低电平再拉高电平） # 74HC595会在这个上升沿将DS引脚上的数据存入移位寄存器D0 # 同时D0原来的数据会顺移到D1，D1的数据位移到D2。。。D6的数据位移到D7 # 而D7的数据已经没有地方储存了，这一位数据会被输出到引脚Q7S上 # 如果Q7S引脚没有被使用，那么这一位的数据就被丢掉了。 # 而如果将Q7S引脚连接到另一块74HC595上的DS引脚， # 那么这一位数据就会继续位移到第二块595芯片的位移寄存器里去。 # 这就是多块595芯片级联的原理。 RPi.GPIO.output(SHCP, False) RPi.GPIO.output(SHCP, True) # 指定第dig位(1-8)数码管显示数字num(0-9)，参数showDotPoint是显示不显示小数点（true/false） # 由于我使用的数码管是共阳数码管，所以设置为低电平的段才会被点亮 # 如果你用的是共阴数码管，那么要将下面的True和False全部颠倒过来，或者统一在前面加上not def showDigit(dig, num, showDotPoint): # 由于在我的硬件连接里，级联的第二块74HC595是负责位选的， # 所以应该先发送8位位选信号，再发送8位段选信号，在连续发完了16位数据以后， # 第一块74HC595的移位寄存器里保存着8位的段选信息，而在第二块74HC595的移位寄存器里保存着8位的位选信息。 # 由于两片芯片的STCP是连在同一个GPIO口的，这个时候如果制造一次STCP的上升沿， # 两块595的位移寄存器里的数据会同时保存到自己的锁存器里，由于我的使能端OE连在GND始终有效，锁存器里的数据就直接输出到了芯片的输出引脚上。 # 第一块芯片的8根输出引脚连接的是所有数码管共用的abcdefg,dp引脚，决定了数码管上显示什么数字。而8只数码管中哪一只会被点亮则取决于哪只数码管的共阳端是高电平。 # 第二块芯片的8根输出引脚连接的是8位数码管的共阳端。只有输出高电平的引脚连接的数码管会点亮。 # 当然如果你喜欢，你也可以让8个数码管全部同时点亮，你只要让第二块595输出11111111就行了。不过显示的数字都是一样的。 # 想显示不同的数字就要进行动态扫描，原理不再说明请参考本系列教程第5篇。 for x in range(1,9): if (x == dig): setBitData(True) else: setBitData(False) if (num == 0) : setBitData(not showDotPoint) # DP setBitData(True) # G setBitData(False) # F setBitData(False) # E setBitData(False) # D setBitData(False) # C setBitData(False) # B setBitData(False) # A elif (num == 1) : setBitData(not showDotPoint) setBitData(True) setBitData(True) setBitData(True) setBitData(True) setBitData(False) setBitData(False) setBitData(True) elif (num == 2) : setBitData(not showDotPoint) setBitData(False) setBitData(True) setBitData(False) setBitData(False) setBitData(True) setBitData(False) setBitData(False) elif (num == 3) : setBitData(not showDotPoint) setBitData(False) setBitData(True) setBitData(True) setBitData(False) setBitData(False) setBitData(False) setBitData(False) elif (num == 4) : setBitData(not showDotPoint) setBitData(False) setBitData(False) setBitData(True) setBitData(True) setBitData(False) setBitData(False) setBitData(True) elif (num == 5) : setBitData(not showDotPoint) setBitData(False) setBitData(False) setBitData(True) setBitData(False) setBitData(False) setBitData(True) setBitData(False) elif (num == 6) : setBitData(not showDotPoint) setBitData(False) setBitData(False) setBitData(False) setBitData(False) setBitData(False) setBitData(True) setBitData(False) elif (num == 7) : setBitData(not showDotPoint) setBitData(True) setBitData(True) setBitData(True) setBitData(True) setBitData(False) setBitData(False) setBitData(False) elif (num == 8) : setBitData(not showDotPoint) setBitData(False) setBitData(False) setBitData(False) setBitData(False) setBitData(False) setBitData(False) setBitData(False) elif (num == 9) : setBitData(not showDotPoint) setBitData(False) setBitData(False) setBitData(True) setBitData(False) setBitData(False) setBitData(False) setBitData(False) # 移位寄存器的8位数据全部传输完毕后，制造一次锁存器时钟引脚的上升沿（先拉低电平再拉高电平） # 74HC595会在这个上升沿将移位寄存器里的8位数据复制到8位的锁存器中（锁存器里原来的数据将被替换） # 到这里为止，这8位数据还只是被保存在锁存器里，并没有输出到数码管上。 # 决定锁存器里的数据是否输出是由“输出使能端口”OE决定的。当OE设置为低电平时，锁存器里数据才会被输出到Q0-Q7这8个输出引脚上。 # 在我的硬件连接里，OE直接连接在了GND上，总是保持低电平，所以移位寄存器的数据一旦通过时钟上升沿进入锁存器，也就相当于输出到LED上了。 RPi.GPIO.output(STCP, True) RPi.GPIO.output(STCP, False) try: # 测试代码 while (True): # 8位数码管显示1-8，不显示小数点 for dig in range(1,9): showDigit(dig, 9-dig, False) time.sleep(0.001) except KeyboardInterrupt: pass # 最后清理GPIO口 RPi.GPIO.cleanup() 资源下载点击下载源码系列文章树莓派GPIO入门01-使用GPIO接口控制发光二极管闪烁树莓派GPIO入门02-GPIO控制LED亮度，制作呼吸灯效果树莓派GPIO入门03-GPIO控制RGB彩色LED灯树莓派GPIO入门04-使用按钮树莓派GPIO入门05-驱动数码管显示数字树莓派GPIO入门06-跟数字湿温度计DHT11通信树莓派GPIO入门07-利用声音传感器制作声控灯树莓派GPIO入门08-使用74HC595芯片驱动数码管（一）树莓派GPIO入门08-使用74HC595芯片驱动数码管（二）【当前文章】树莓派GPIO入门09-使用MAX7219芯片驱动8位数码管树莓派GPIO入门10-使用TLC5940芯片输出多路PWM树莓派GPIO入门11-驱动液晶屏幕（一）树莓派GPIO入门11-驱动液晶屏幕（二）
之前我们学习了如何驱动数码管显示数字。但是这种做法有两个缺点。需要占用大量IO口，在那篇文章里，我们为了驱动4位数码管一共使用了12个IO口。如果需要驱动8位数码管则至少需要16个IO口。系统资源占用过多。还是在那篇文章里，为了实现数码管的动态扫描显示，需要不停地高速操作IO口，这对单任务的单片机比如51单片机来说问题并不太大，因为单任务的单片机的执行时序是由晶振来决定的，动态扫描的时间间隔可以控制地非常精确，数码管的显示会很稳定，不会出现闪烁的现象。但对于运行着多任务的Linux操作系统的树莓派来说，对IO口的大量反复操作以及大量使用sleep语句会导致CPU占用过多，最后导致动态扫描的间隔时间不均匀，体现为数码管显示不稳定，有明显闪烁的现象。为了改善这个问题我在那篇文章的最后还尝试用c语言来代替python语言，可实际效果有限。使用GPIO直接驱动数码管时，上面这两个问题是不可避免的。想解决这两个问题，我们需要借助外部芯片的帮助。接下来我将用几个篇幅来介绍如何使用显示驱动芯片74HC595来驱动数码管的方法。同时我们也可以学习到串行数据传输的方法以及一些数字芯片通用的一些概念，比如时钟引脚，上升沿，锁存，移位寄存等。理解了这些通用的概念对学习使用其他芯片是很有帮助的。本文最终效果在一块74HC595芯片的配合下，只需要3个GPIO就能驱动一只8段数码管。（直接用GPIO驱动需要至少8个GPIO口）视频演示http://v.youku.com/v_show/id_XMTI1NTAxMTMyMA==.html?spm=a2hzp.8253869.0.0硬件74HC595芯片一只（0.5元一只）共阳（或共阴）数码管一只限流电阻一只，330欧就够了。原理说明先转一段百度百科上74HC595芯片的简介。74HC595具有8位移位寄存器和一个存储器，三态输出功能。 移位寄存器和存储器有相互独立的时钟。数据在SH_cp（移位寄存器时钟输入）的上升沿输入到移位寄存器中，在ST_cp（存储器时钟输入）的上升沿输入到存储寄存器中去。如果两个时钟连在一起，则移位寄存器总是比存储寄存器早一个脉冲。移位寄存器有一个串行移位输入（Ds），和一个串行输出（Q7’）,和一个异步的低电平复位，存储寄存器有一个并行8位的，具备三态的总线输出，当使能OE时（为低电平），存储寄存器的数据输出到总线。看不懂吧，没关系，我们先看一下芯片的引脚图：分别解释一下：GND接地，VCC接5V电源，这个就不用说了。Q0-Q7这8根引脚是芯片的输出引脚，直接跟数码管的8段引脚相连。对应关系要看你怎么接线和写代码时传送数据的顺序了。DS是串行输入引脚，所谓串行就是使数据在一根信号线上按顺序一位一位地传输，就像一串糖葫芦。这个引脚我们接到树莓派任意一个GPIO口上（输出模式）。SHCP是移位寄存器的时钟引脚。听上去有点复杂，其实很简单。74HC595内部有一个8位的移位寄存器用来保存从DS引脚输入的数据。那么74HC595怎么知道什么时候该从DS引脚上取数据了呢？正是通过SHCP这个时钟引脚来实现的。只有在SHCP发生一次上升沿的时候，74HC595才会从DS引脚上取得当前的数据（高/低电平）并把取到的这一位数据保存到移位寄存器里。同样的，这个引脚也接到树莓派任意一个GPIO口上。当我们向芯片发送数据时，要先在DS引脚上准备好要传送的数据，然后制造一次SHCP引脚的上升沿（先拉低电平再拉高电平），74HC595会在这个上升沿将DS引脚上的数据存入移位寄存器D0，同时D0原来的数据会顺移到D1，D1的数据位移到D2。。。D6的数据位移到D7。而原先D7的数据已经没有地方储存了，这一位数据会被输出到引脚Q7S上。这个引脚的作用我们下一篇再说，本文暂时用不到这个引脚。（注意这里说的不是输出引脚Q0-Q7，而是指内部的8位移位寄存器里每一个“小房间”，芯片手册上并没有给这些小房间编号，这里为了说明方便进行了编号）STCP是芯片内部另外一个8位锁存寄存器的时钟引脚。当移位寄存器的8位数据全部传输完毕后，制造一次锁存器时钟引脚的上升沿（先拉低电平再拉高电平）。74HC595会在这个上升沿将移位寄存器里的8位数据复制到锁存器中（锁存器里原来的数据将被替换）。注意，到这里为止，这8位数据还只是被保存在锁存器里，并没有输出到数码管上。这个引脚同样连接到树莓派任意一个GPIO口上即可。OE是输出使能引脚，在其他芯片里也很常见。作用是控制锁存器里的数据是否最终输出到Q0-Q7输出引脚上。低电平时输出，高电平时不输出（既不是高电平，也不是低电平而是高阻态，不通电）。本例为了方便直接接在GND上使其一直保持低电平输出数据。MR是用来重置内部寄存器的引脚。低电平时重置内部寄存器(MemoryReset?)。本例为了方便直接连接在Vcc上一直保持高电平。Q7S引脚，串行输出引脚，本文不使用，下一篇再解释它的作用。关于锁存器。顾名思义就是将数据保存并锁定。一旦进入了锁存器，除非断电或重置数据（MR口设置为低电平），锁存器的数据不会再改变。好处是，当你需要更新数据时，将数据串行输入移位寄存器的过程中，锁存器里的数据不会有任何影响，也就不会有闪烁了。一直到移位寄存器8位数据准备完毕，再制造一次STCP的上升沿一次性更新锁存器的数据，更新输出。另外，我做了一个动画帮助你理解整个过程。硬件连接模块1引脚模块2引脚74HC595Q0数码管DP74HC595Q1数码管G74HC595Q2数码管F74HC595Q3数码管E74HC595Q4数码管D74HC595Q5数码管C74HC595Q6数码管B74HC595Q7数码管A74HC595DS树莓派GPIO1374HC595SHCP树莓派GPIO1974HC595SHTP树莓派GPIO2674HC595VCC,MR树莓派VCC74HC595GND,OE树莓派GND数码管1位共阳极树莓派VCC注意，共阳数码管的共阳极连接5V电源时要串联一只限流电阻防止数码管电流过大损坏电阻。如果你用的是共阴极数码管就没关系了，74HC595输出的电流是控制好的可以直接连到阳极上。我手头上没有单独的1位数码管，就用4位数码管代替了，本文只接通数码管DIG1的共阳极做演示。代码（Python）#!/usr/bin/env python # encoding: utf-8 import RPi.GPIO import time # 串行数据输入引脚连接的GPIO口 DS = 13 # 移位寄存器时钟控制引脚连接的GPIO口——上升沿有效 SHCP = 19 # 数据锁存器时钟控制引脚连接的GPIO口——上升沿有效 STCP = 26 RPi.GPIO.setmode(RPi.GPIO.BCM) RPi.GPIO.setup(DS, RPi.GPIO.OUT) RPi.GPIO.setup(STCP, RPi.GPIO.OUT) RPi.GPIO.setup(SHCP, RPi.GPIO.OUT) RPi.GPIO.output(STCP, False) RPi.GPIO.output(SHCP, False) # 通过串行数据引脚向74HC595的传送一位数据 def setBitData(data): # 准备好要传送的数据 RPi.GPIO.output(DS, data) # 制造一次移位寄存器时钟引脚的上升沿（先拉低电平再拉高电平） # 74HC595会在这个上升沿将DS引脚上的数据存入移位寄存器D0 # 同时D0原来的数据会顺移到D1，D1的数据位移到D2。。。D6的数据位移到D7 # 而D7的数据已经没有地方储存了，这一位数据会被输出到引脚Q7S上 RPi.GPIO.output(SHCP, False) RPi.GPIO.output(SHCP, True) # 指定数码管显示数字num(0-9)，第2个参数是显示不显示小数点（true/false） # 由于我使用的数码管是共阳数码管，所以设置为低电平的段才会被点亮 # 如果你用的是共阴数码管，那么要将下面的True和False全部颠倒过来，或者统一在前面加上not def showDigit(num, showDotPoint): if (num == 0) : setBitData(not showDotPoint) # DP setBitData(True) # G setBitData(False) # F setBitData(False) # E setBitData(False) # D setBitData(False) # C setBitData(False) # B setBitData(False) # A elif (num == 1) : setBitData(not showDotPoint) setBitData(True) setBitData(True) setBitData(True) setBitData(True) setBitData(False) setBitData(False) setBitData(True) elif (num == 2) : setBitData(not showDotPoint) setBitData(False) setBitData(True) setBitData(False) setBitData(False) setBitData(True) setBitData(False) setBitData(False) elif (num == 3) : setBitData(not showDotPoint) setBitData(False) setBitData(True) setBitData(True) setBitData(False) setBitData(False) setBitData(False) setBitData(False) elif (num == 4) : setBitData(not showDotPoint) setBitData(False) setBitData(False) setBitData(True) setBitData(True) setBitData(False) setBitData(False) setBitData(True) elif (num == 5) : setBitData(not showDotPoint) setBitData(False) setBitData(False) setBitData(True) setBitData(False) setBitData(False) setBitData(True) setBitData(False) elif (num == 6) : setBitData(not showDotPoint) setBitData(False) setBitData(False) setBitData(False) setBitData(False) setBitData(False) setBitData(True) setBitData(False) elif (num == 7) : setBitData(not showDotPoint) setBitData(True) setBitData(True) setBitData(True) setBitData(True) setBitData(False) setBitData(False) setBitData(False) elif (num == 8) : setBitData(not showDotPoint) setBitData(False) setBitData(False) setBitData(False) setBitData(False) setBitData(False) setBitData(False) setBitData(False) elif (num == 9) : setBitData(not showDotPoint) setBitData(False) setBitData(False) setBitData(True) setBitData(False) setBitData(False) setBitData(False) setBitData(False) # 移位寄存器的8位数据全部传输完毕后，制造一次锁存器时钟引脚的上升沿（先拉低电平再拉高电平） # 74HC595会在这个上升沿将移位寄存器里的8位数据复制到8位的锁存器中（锁存器里原来的数据将被替换） # 到这里为止，这8位数据还只是被保存在锁存器里，并没有输出到数码管上。 # 决定锁存器里的数据是否输出是由“输出使能端口”OE决定的。当OE设置为低电平时，锁存器里数据才会被输出到Q0-Q7这8个输出引脚上。 # 在我的硬件连接里，OE直接连接在了GND上，总是保持低电平，所以移位寄存器的数据一旦通过时钟上升沿进入锁存器，也就相当于输出到LED上了。 RPi.GPIO.output(STCP, True) RPi.GPIO.output(STCP, False) try: # 测试代码 # 从0显示到9，不显示小数点 for x in range(0,10): showDigit(x, False) time.sleep(0.2) # 再从0显示到9，显示小数点 for y in range(0,10): showDigit(y, True) time.sleep(0.2) except KeyboardInterrupt: pass # 最后清理GPIO口 # 清理了IO是将所有使用中的IO口释放，并全部设置为输入模式 # 你会发现最后设置的数据在清理了IO口以后还会继续正常显示 # 这是因为数据一旦存入锁存器，除非断电或重置数据（MR口设置为低电平）， # 否则最后设置的数据会一直保留在74HC595芯片中。也就是被“锁存”了。 RPi.GPIO.cleanup() 资源下载点击下载源码系列文章树莓派GPIO入门01-使用GPIO接口控制发光二极管闪烁树莓派GPIO入门02-GPIO控制LED亮度，制作呼吸灯效果树莓派GPIO入门03-GPIO控制RGB彩色LED灯树莓派GPIO入门04-使用按钮树莓派GPIO入门05-驱动数码管显示数字树莓派GPIO入门06-跟数字湿温度计DHT11通信树莓派GPIO入门07-利用声音传感器制作声控灯树莓派GPIO入门08-使用74HC595芯片驱动数码管（一）【当前文章】树莓派GPIO入门08-使用74HC595芯片驱动数码管（二）树莓派GPIO入门09-使用MAX7219芯片驱动8位数码管树莓派GPIO入门10-使用TLC5940芯片输出多路PWM树莓派GPIO入门11-驱动液晶屏幕（一）树莓派GPIO入门11-驱动液晶屏幕（二）
本文我们利用树莓派的GPIO口来跟数字湿温度计模块DHT11进行通信取得温度和湿度数据并显示在数码管上，通过按钮来切换显示温度或湿度。最终效果硬件数码管杜邦线面包板按钮1只数字湿温度计DHT11模块。（我们这个教程里用到的所有电子元件均可在淘宝购买到）原理说明原始的DHT11模块有4根引脚，长成这个样子：由图可以看出4根引脚里除了VCC，GND，DATA以外，还有一个引脚是N/A，也就是不使用。（不使用引出来干嘛？好看吗？不解）本文使用的是又被封装了一次的模块，去掉了无用的引脚。其他3个引脚保留。功能完全一样，所以如果你手头上的DHT11是有4根引脚的请忽略N/A针脚，其他的跟我使用的这种完全通用。3个针脚分别连接到3.3v电源，GND和任意GPIO口上。根据数据手册（文末提供下载）的说明，总线（DATA引脚）在空闲状态需要保持高电平状态，所以我们除了将DATA引脚接到一个GPIO口上，还要通过一个4.7K（经实测2K左右的就够了）的电阻将DATA引脚并联到VCC上。这个电阻也称上拉电阻，电阻就是一般的电阻，只是在这里起的作用是上拉电平的作用所以称之为上拉电阻。与DHT11通信时，发送和接收信息都在一根DATA口上，这种只用1根总线的数据传输方式称为单总线模式。向DHT11发送数据时，GPIO口需要设置为OUTPUT模式，从DHT11接收数据时GPIO口需要切换成INPUT模式。具体通信的时序如下：由于有上拉电阻存在，总线（DATA）空闲状态为高电平。树莓派GPIO口设置为OUTPUT模式。树莓派向DHT11发送起始信号。方式是GPIO口设置低电平并持续一段时间，根据数据手册的说明，这段时间必须大于18毫秒，保证DHT11能检测到起始信号。树莓派起始信号输出完毕，切换到输入模式，等待DHT11响应。一旦切换到输入模式GPIO口就不再输出电平信号，总线处于释放状态，由于有上拉电阻的存在，总线被拉回高电平。在总线被拉回至高电平通知DHT11主机已经准备好接受数据以后，DHT11还会继续等待20-40us左右以后才会开始发送反馈信号。DHT11开始发送反馈信号，总线被DHT11拉低，持续80us左右。这个持续了80us左右的低电平的反馈信号结束以后，DHT11又会将DATA口拉回高电平并再次持续80us左右。DHT11开始正式传输40bit的二进制数据（0或1）。每一个bit的数据（0或者1）总是由一段持续50us的低电平信号开始，再由一段持续26us-28us（数据0）或者持续70us（数据1）的高电平结束。一直到40位数据传输完毕。这40位的数据内容是：8bit湿度整数数据 + 8bit湿度小数数据 + 8bi温度整数数据 + 8bit温度小数数据 + 8bit校验和。而校验和数据应该等于“湿度整数数据+湿度小数数据+温度整数数据+温度小数数据”所得结果的末8位。我们的程序就应该遵循上述时序来与DHT11进行数据通信。硬件连接下面的连接图只标出了DHT11的连线和上拉电阻的连线方法。数码管和按钮的连线请参考上一篇。关键代码void readDHT11() { int i,j,cnt = 0; for (j = 0; j < RETRY; ++j) { for (i = 0; i < 5; ++i) { data[i] = 0; } // GPIO口模式设置为输出模式 pinMode (DATA, OUTPUT) ; // 先拉高DATA一段时间，准备发送开始指令 digitalWrite (DATA, HIGH); usleep(500000); // 500 ms // 拉低DATA口，输出开始指令（至少持续18ms） digitalWrite (DATA, LOW); usleep(TIME_START); digitalWrite (DATA, HIGH); // 开始指令输出完毕，切换到输入模式，等待DHT11输出信号。 // 由于有上拉电阻的存在，所以DATA口会维持高电平。 pinMode (DATA, INPUT); // 在DATA口被拉回至高电平通知DHT11主机已经准备好接受数据以后， // DHT11还会继续等待20-40us左右以后才会开始发送反馈信号，所以我们把这段时间跳过去 // 如果长时间（1000us以上）没有低电平的反馈表示有问题，结束程序 cnt=0; while (digitalRead(DATA) == HIGH) { cnt++; if (cnt > MAXCNT) { printf("DHT11未响应，请检查连线是否正确，元件是否正常工作。\n"); exit(1); } } // 这个反馈响应信号的低电平会持续80us左右，但我们不需要精确计算这个时间 // 只要一直循环检查DATA口的电平有没有恢复成高电平即可 cnt=0; while (digitalRead(DATA) == LOW) { cnt++; if (cnt > MAXCNT) { printf("DHT11未响应，请检查连线是否正确，元件是否正常工作。\n"); exit(1); } } // 这个持续了80us左右的低电平的反馈信号结束以后，DHT11又会将DATA口拉回高电平并再次持续80us左右 // 然后才会开始发送真正的数据。所以跟上面一样，我们再做一个循环来检测这一段高电平的结束。 cnt=0; while (digitalRead(DATA) == HIGH) { cnt++; if (cnt > MAXCNT) { printf("DHT11未响应，请检查连线是否正确，元件是否正常工作。\n"); exit(1); } } // ### ### ### ### ### ### ### 40bit的数据传输开始 ### ### ### ### ### ### ### # for (i = 0; i < 40; i++) { // 每一个bit的数据（0或者1）总是由一段持续50us的低电平信号开始 // 跟上面一样我们用循环检测的方式跳过这一段 while (digitalRead(DATA) == LOW) { } // 接下来的高电平持续的时间是判断该bit是0还是1的关键。 // 根据DHT11的说明文档，我们知道 这段高电平持续26us-28us左右的话表示这是数据0。 // 如果这段高电平持续时间为70us左右表示这是数据1。 // 方法1：在高电平开始的时候记下时间，在高电平结束的时候再记一个时间， // 通过计算两个时间的间隔就能得知是数据0还是数据1了。 // 方法2：在高电平开始的以后我们延时40us，然后再次检测DATA口: // (a) 如果此时DATA口是低电平，表示当前位的数据已经发送完并进入下一位数据的传输准备阶段（低电平50us）了。 // 由于数据1的高电平持续时间是70us，所以如果是数据1，此时DATA口应该还是高电平才对， // 据此我们可以断言刚才传输的这一位数据是0。 // (b) 如果延时40us以后DATA口仍然是高电平，那么我们可以断言这一位数据一定是1了，因为数据0只会持续26us。 // 方法3：循环检测电平状态并计数，每检查一次如果电平状态没变就让计数器加一，一直到电平状态变成低电平为止。 // 数据0的高电平持续时间短，所以计数一定比数据1的计数少，由于微秒级别的延时太短，这个计数会有一定误差。 // 我们需要先在自己的树莓派上用printf打印出每一位数据计数的结果，然后观察计数结果来设定一个阈值， // 高于这个阈值的就认为的数据1，低于这个值的就认为是数据0。 // 我们这里采用简单易行的方法3。 // 实际上，由于要求的时序太短，在树莓派上很难通过方法1和方法2实现。这也是我使用c而不是python的原因。 // 特别是方法2，因为linux里有一个系统级的延时函数usleep，单位确实是微秒。 // 貌似用usleep(40)就可以了，实际测试的结果是延时远远超过40us。其原因是usleep这个函数的延时方法是 // 暂停当前进程并放开cpu权限让cpu可以在这段时间里去处理其他任务。这样做的好处是不会浪费cpu资源， // 但问题是当系统将cpu权限交还给我们的进程的这个过程本身就要耗费若干ms（毫秒哦） // 所以导致usleep这个函数实际上没有办法做到延时几十微秒。 cnt=0; while (digitalRead(DATA) == HIGH) { // 当所有数据传输完以后，DHT11会放开总线，DATA口就会被上拉电阻一直拉高。 // 所以如果超过一定时间电平还没有被拉低就表示所有的数据已经传输完毕，停止检测。 cnt++; if (cnt > MAXCNT) { break; } } if (cnt > MAXCNT) { break; } // 将当前位的计数保存起来 bits[i] = cnt; } // 整理数据，将位数据转成5个数字 for (i = 0; i < 40; ++i) { data[i/8] <<= 1; if (bits[i] > VAL) { data[i/8] |= 1; } //下面这句话就是用来测试自己的设备应该设定多少阈值的测试代码 //printf("bits[%d] = %d (%d) \n", i, bits[i], bits[i]>200?1:0 ); } // 往屏幕上输出取得的数据 for (i = 0; i < 5; ++i) { printf("data[%d] = %d \n", i, data[i] ); } // 用校验和来检查接收数据是否完整 if (data[4] == ((data[0] + data[1] + data[2] + data[3]) & 0xFF) ) { printf("校验成功！ \n"); // 将湿度，温度数据赋值给显示用的湿度，温度变量 shidu = data[0]; wendu = data[2]; break; } else { printf("校验不成功，重新取值！ \n"); // 校验不成功，重新取值，连续10次取值不成功就放弃。一般连线和逻辑正确的话连续10次取值出错是不可能的。 continue; } } }校验和下图是实际运行时利用校验和检测到数据出现了接收错误的情况资源下载点击下载C源码点击下载DHT11模块产品手册系列文章树莓派GPIO入门01-使用GPIO接口控制发光二极管闪烁树莓派GPIO入门02-GPIO控制LED亮度，制作呼吸灯效果树莓派GPIO入门03-GPIO控制RGB彩色LED灯树莓派GPIO入门04-使用按钮树莓派GPIO入门05-驱动数码管显示数字树莓派GPIO入门06-跟数字湿温度计DHT11通信【当前文章】树莓派GPIO入门07-利用声音传感器制作声控灯树莓派GPIO入门08-使用74HC595芯片驱动数码管（一）树莓派GPIO入门08-使用74HC595芯片驱动数码管（二）树莓派GPIO入门09-使用MAX7219芯片驱动8位数码管树莓派GPIO入门10-使用TLC5940芯片输出多路PWM树莓派GPIO入门11-驱动液晶屏幕（一）树莓派GPIO入门11-驱动液晶屏幕（二）
这次我们用树莓派的GPIO口驱动数码管来显示数字，进而制作一个简单的电子钟，通过按钮来切换显示时间或日期。最终效果硬件数码管一只。因为需要显示日期时间所以需要能显示4个数字的数码管，也称4位数码管。淘宝买1块多钱一只。注意数码管有共阳和共阴的。我这里使用的是共阳的。至于区别，下面的原理说明里会详细解释。杜邦线若干。面包板一块。按钮1只。原理说明标准的数码管从显示内容上分7段和8段数码管两种。8段比7段多一个右下角的小数点。还有一些其他特殊的如可以显示米字形的数码管不在本文讨论范围内，其实原理都是一样的。8段数码管由8个发光二极管组成，其中7个用于组成数字，1个用于显示小数点。每一根的编号如下图的右上角所示(A-G,DP)。数码管从电源极性上分共阳和共阴两种。解释一下，如果数码管上每一个独立的发光二极管都单独引出两根引脚，一根接正极(阳)一根接负极(阴)，那么一个8段数码管就需要16根引脚来控制。但其实这8段数码管完全可以在内部共用一个阳级，只控制各段发光二极管的阴级联通即可，这就是共阳。反之亦然，叫共阴。共阳或共阴的每个8段数码管只需要引出9个引脚，1个阳(阴)级接到树莓派vcc(gnd)上，另外8个分别连到gpio口上，通过控制io口高低电平即可显示所需数字。比如一只共阳数码管想显示数字1，看LED编号图可知需要点亮b段和c段，其他全灭。那么连到共阳端引脚的io口输出高电平，连到引脚b、c的io口输出低电平，连到引脚a、d、e、f、g、dp的io口均输出高电平即可。写成代码就是：# 定义各段发光二极管对应的GPIO口 LED_A = 26 LED_B = 19 LED_C = 13 LED_D = 6 LED_E = 5 LED_F = 11 LED_G = 9 LED_DP = 10 # 定义数码管共阳极对应的GPIO口 VCC = 12 # 避免闪烁，在输出数字字形信号前先拉低共阳端，关闭显示 RPi.GPIO.output(VCC, False) # 输出数字1的字形信号 RPi.GPIO.output(LED_A, True) RPi.GPIO.output(LED_B, False) RPi.GPIO.output(LED_C, False) RPi.GPIO.output(LED_D, True) RPi.GPIO.output(LED_E, True) RPi.GPIO.output(LED_F, True) RPi.GPIO.output(LED_G, True) RPi.GPIO.output(LED_DP, True) # 最后拉高共阳段，显示数字 RPi.GPIO.output(VCC, True)本文使用的数码管是8段共阳4位(4个数字)数码管，型号是F3461BH。上面说了共阳数码管每个数字需要9个引脚来控制，那么4个数字就需要36个引脚吗？显然不现实，树莓派的io口也完全不够用。这就引出另一个概念，静态显示和动态扫描显示。静态显示，就是前面说的每一个数字需要占用8个io口，每多一个数字就需要额外的8个io口，如果数字位数不多，io口够用的话，这样做完全没问题。实际应用中往往需要显示多个数字，io口基本上是不够用的。这就需要动态扫描显示了。下面摘一段百度百科关于动态扫描显示的说明(稍有改动):数码管动态显示接口是单片机中应用最为广泛的一种显示方式之一，动态驱动是将所有数码管的8个显示笔划"a,b,c,d,e,f,g,dp"的同名端连在一起引出8个引脚，每个数字再单独引出共阳(阴)端，这样总引脚数就只要8 + 数字个数即可，本文使用的8段4位数码管正是引出了12个引脚。至于哪个引脚对应哪一段，哪几个引脚分别对应各数字的共阳(阴)端，就需要商家提供电路图了。当然也可以自己慢慢试，这不在本文讨论范围，大家可以自己摸索。当树莓派输出8个段信号时，所有数码管都会接收到相同的信号，但究竟是哪个数码管会显示出字形，取决于这个数码管对应的共阳(阴)极(后统称位选端)有无导通。所以我们只要将需要显示的数码管的位选端选通，该位就显示出字形，没有选通的数码管就不会亮。通过分时轮流控制各个数码管的的位选端，就使各个数码管轮流受控显示，这就是动态驱动。在轮流显示过程中，每位数码管的点亮时间为1～2ms，由于人的视觉暂留现象及发光二极管的余辉效应，尽管实际上各位数码管并非同时点亮，但只要扫描的速度足够快，给人的印象就是一组稳定的显示数据，不会有闪烁感，动态显示的效果和静态显示是一样的，能够节省大量的I/O端口，而且功耗更低。综上，比如我们想要在4位共阳数码管上显示1234这4个数字，要做的就是：避免闪烁，在输出数字字形信号前先拉低所有4个共阳极，关闭所有显示。拉低1号数码管的共阳极，关闭显示。输出1号数码管上希望显示的数字字形信号。只拉高1号数码管的共阳极，让1号数码管显示上面设置好的数字字形。延时一段时间（1ms左右）拉低1号数码管的共阳极，关闭显示。输出2号数码管上希望显示的数字字形信号。只拉高2号数码管的共阳极，让2号数码管显示上面设置好的数字字形。延时一段时间（1ms左右）拉低2号数码管的共阳极，关闭显示。输出3号数码管上希望显示的数字字形信号。只拉高3号数码管的共阳极，让3号数码管显示上面设置好的数字字形。延时一段时间（1ms左右）拉低3号数码管的共阳极，关闭显示。输出4号数码管上希望显示的数字字形信号。只拉高4号数码管的共阳极，让4号数码管显示上面设置好的数字字形。延时一段时间（1ms左右）返回step2，循环执行。取得日期和时间的方法不是本文的重点，大家自己看代码。硬件连接代码#!/usr/bin/env python # encoding: utf-8 import RPi.GPIO import time # 定义单个数码管各段led对应的GPIO口 LED_A = 26 LED_B = 19 LED_C = 13 LED_D = 6 LED_E = 5 LED_F = 11 LED_G = 9 LED_DP = 10 # 定义1到4号数码管阳极对应的GPIO口 DIGIT1 = 12 DIGIT2 = 16 DIGIT3 = 20 DIGIT4 = 21 # 定义按钮输入的GPIO口 btn = 27 RPi.GPIO.setmode(RPi.GPIO.BCM) RPi.GPIO.setup(LED_A, RPi.GPIO.OUT) RPi.GPIO.setup(LED_B, RPi.GPIO.OUT) RPi.GPIO.setup(LED_C, RPi.GPIO.OUT) RPi.GPIO.setup(LED_D, RPi.GPIO.OUT) RPi.GPIO.setup(LED_E, RPi.GPIO.OUT) RPi.GPIO.setup(LED_F, RPi.GPIO.OUT) RPi.GPIO.setup(LED_G, RPi.GPIO.OUT) RPi.GPIO.setup(LED_DP, RPi.GPIO.OUT) RPi.GPIO.setup(DIGIT1, RPi.GPIO.OUT) RPi.GPIO.setup(DIGIT2, RPi.GPIO.OUT) RPi.GPIO.setup(DIGIT3, RPi.GPIO.OUT) RPi.GPIO.setup(DIGIT4, RPi.GPIO.OUT) RPi.GPIO.output(DIGIT1, True) RPi.GPIO.output(DIGIT2, True) RPi.GPIO.output(DIGIT3, True) RPi.GPIO.output(DIGIT4, True) RPi.GPIO.setup(btn, RPi.GPIO.IN, pull_up_down=RPi.GPIO.PUD_UP) # 指定no(1-4)号数码管显示数字num(0-9)，第三个参数是显示不显示小数点（true/false） def showDigit(no, num, showDotPoint): # 先将正极拉低，关掉显示 RPi.GPIO.output(DIGIT1, False) RPi.GPIO.output(DIGIT2, False) RPi.GPIO.output(DIGIT3, False) RPi.GPIO.output(DIGIT4, False) if (num == 0) : RPi.GPIO.output(LED_A, False) RPi.GPIO.output(LED_B, False) RPi.GPIO.output(LED_C, False) RPi.GPIO.output(LED_D, False) RPi.GPIO.output(LED_E, False) RPi.GPIO.output(LED_F, False) RPi.GPIO.output(LED_G, True) RPi.GPIO.output(LED_DP, not showDotPoint) elif (num == 1) : RPi.GPIO.output(LED_A, True) RPi.GPIO.output(LED_B, False) RPi.GPIO.output(LED_C, False) RPi.GPIO.output(LED_D, True) RPi.GPIO.output(LED_E, True) RPi.GPIO.output(LED_F, True) RPi.GPIO.output(LED_G, True) RPi.GPIO.output(LED_DP, not showDotPoint) elif (num == 2) : RPi.GPIO.output(LED_A, False) RPi.GPIO.output(LED_B, False) RPi.GPIO.output(LED_C, True) RPi.GPIO.output(LED_D, False) RPi.GPIO.output(LED_E, False) RPi.GPIO.output(LED_F, True) RPi.GPIO.output(LED_G, False) RPi.GPIO.output(LED_DP, not showDotPoint) elif (num == 3) : RPi.GPIO.output(LED_A, False) RPi.GPIO.output(LED_B, False) RPi.GPIO.output(LED_C, False) RPi.GPIO.output(LED_D, False) RPi.GPIO.output(LED_E, True) RPi.GPIO.output(LED_F, True) RPi.GPIO.output(LED_G, False) RPi.GPIO.output(LED_DP, not showDotPoint) elif (num == 4) : RPi.GPIO.output(LED_A, True) RPi.GPIO.output(LED_B, False) RPi.GPIO.output(LED_C, False) RPi.GPIO.output(LED_D, True) RPi.GPIO.output(LED_E, True) RPi.GPIO.output(LED_F, False) RPi.GPIO.output(LED_G, False) RPi.GPIO.output(LED_DP, not showDotPoint) elif (num == 5) : RPi.GPIO.output(LED_A, False) RPi.GPIO.output(LED_B, True) RPi.GPIO.output(LED_C, False) RPi.GPIO.output(LED_D, False) RPi.GPIO.output(LED_E, True) RPi.GPIO.output(LED_F, False) RPi.GPIO.output(LED_G, False) RPi.GPIO.output(LED_DP, not showDotPoint) elif (num == 6) : RPi.GPIO.output(LED_A, False) RPi.GPIO.output(LED_B, True) RPi.GPIO.output(LED_C, False) RPi.GPIO.output(LED_D, False) RPi.GPIO.output(LED_E, False) RPi.GPIO.output(LED_F, False) RPi.GPIO.output(LED_G, False) RPi.GPIO.output(LED_DP, not showDotPoint) elif (num == 7) : RPi.GPIO.output(LED_A, False) RPi.GPIO.output(LED_B, False) RPi.GPIO.output(LED_C, False) RPi.GPIO.output(LED_D, True) RPi.GPIO.output(LED_E, True) RPi.GPIO.output(LED_F, True) RPi.GPIO.output(LED_G, True) RPi.GPIO.output(LED_DP, not showDotPoint) elif (num == 8) : RPi.GPIO.output(LED_A, False) RPi.GPIO.output(LED_B, False) RPi.GPIO.output(LED_C, False) RPi.GPIO.output(LED_D, False) RPi.GPIO.output(LED_E, False) RPi.GPIO.output(LED_F, False) RPi.GPIO.output(LED_G, False) RPi.GPIO.output(LED_DP, not showDotPoint) elif (num == 9) : RPi.GPIO.output(LED_A, False) RPi.GPIO.output(LED_B, False) RPi.GPIO.output(LED_C, False) RPi.GPIO.output(LED_D, False) RPi.GPIO.output(LED_E, True) RPi.GPIO.output(LED_F, False) RPi.GPIO.output(LED_G, False) RPi.GPIO.output(LED_DP, not showDotPoint) if (no == 1) : RPi.GPIO.output(DIGIT1, True) elif (no == 2) : RPi.GPIO.output(DIGIT2, True) elif (no == 3) : RPi.GPIO.output(DIGIT3, True) elif (no == 4) : RPi.GPIO.output(DIGIT4, True) try: t=0.005 while True: # 按钮按下时显示日期，否则显示时间 # 为了区别左右的数字，让第二个数码管的小数点显示出来 #（本来应该是一个冒号，我们这个数码管没有，就用小数点代替了） if (RPi.GPIO.input(btn) == 1): time.sleep(t) showDigit(1, int(time.strftime("%H",time.localtime(time.time()))) / 10, False) time.sleep(t) showDigit(2, int(time.strftime("%H",time.localtime(time.time()))) % 10, True) time.sleep(t) showDigit(3, int(time.strftime("%M",time.localtime(time.time()))) / 10, False) time.sleep(t) showDigit(4, int(time.strftime("%M",time.localtime(time.time()))) % 10, False) else: time.sleep(t) showDigit(1, int(time.strftime("%m",time.localtime(time.time()))) / 10, False) time.sleep(t) showDigit(2, int(time.strftime("%m",time.localtime(time.time()))) % 10, True) time.sleep(t) showDigit(3, int(time.strftime("%d",time.localtime(time.time()))) / 10, False) time.sleep(t) showDigit(4, int(time.strftime("%d",time.localtime(time.time()))) % 10, False) except KeyboardInterrupt: pass # 最后清理GPIO口（不做也可以，建议每次程序结束时清理一下，好习惯） RPi.GPIO.cleanup()性能改善当你照着这篇文章成功在数码管上显示出数字后，你可能会郁闷的发现数字有一点点闪烁，显示的不是非常稳定，这种情况在树莓派1代上更明显。python库对gpio口不停操作时对cpu的占用率很高，而一旦cpu的占用率一高，就不能保证延时函数的准确性，而在动态扫描时延时的不准确会导致数码管的亮度不稳定，比如第一只数码管显示了2ms而第二只显示了1ms的话，第一只数码管的亮度就会略高于第二只数码管，而参差不齐的延时最后就表现为数字显示有闪烁的感觉。python库本身翻转gpio口的效率就不高，有兴趣可以参照这个页面，有各种语言各种库对gpio的操作速度比较。比较结果是c的wiringPi库完胜其他方式。其实python库已经是表现不错的了，但还是被c甩了N条街。python翻转io口的频率在70kHz左右，而wiringPi则可以达到4MHz！这个已经是数量级上的差距了。抱着试一试的心情，我用c语言的wiringPi库把上面这个程序重写了一遍，代码是这个样子的：#include <wiringPi.h> #include <unistd.h> #include <time.h> // 定义单个数码管各段led对应的GPIO口 // 使用命令 "gpio readall" 来获取当前pi版本对应的各引脚的wiringPi和BCM的编号 // 再本程序中应该使用wiringPi编号 // 我的pi2 Mode B执行结果如下：（wPi列就是wiringPi编号） // 之前Python版本的代码使用的是BCM编号，所以在不改变硬件接线的情况下，我们需要把原来BCM编号改成对应的wiringPi编号。 /* +-----+-----+---------+------+---+---Pi 2---+---+------+---------+-----+-----+ | BCM | wPi | Name | Mode | V | Physical | V | Mode | Name | wPi | BCM | +-----+-----+---------+------+---+----++----+---+------+---------+-----+-----+ | | | 3.3v | | | 1 || 2 | | | 5v | | | | 2 | 8 | SDA.1 | IN | 1 | 3 || 4 | | | 5V | | | | 3 | 9 | SCL.1 | IN | 1 | 5 || 6 | | | 0v | | | | 4 | 7 | GPIO. 7 | IN | 1 | 7 || 8 | 1 | ALT0 | TxD | 15 | 14 | | | | 0v | | | 9 || 10 | 1 | ALT0 | RxD | 16 | 15 | | 17 | 0 | GPIO. 0 | OUT | 0 | 11 || 12 | 0 | IN | GPIO. 1 | 1 | 18 | | 27 | 2 | GPIO. 2 | IN | 0 | 13 || 14 | | | 0v | | | | 22 | 3 | GPIO. 3 | IN | 0 | 15 || 16 | 0 | IN | GPIO. 4 | 4 | 23 | | | | 3.3v | | | 17 || 18 | 0 | IN | GPIO. 5 | 5 | 24 | | 10 | 12 | MOSI | IN | 0 | 19 || 20 | | | 0v | | | | 9 | 13 | MISO | IN | 0 | 21 || 22 | 0 | IN | GPIO. 6 | 6 | 25 | | 11 | 14 | SCLK | IN | 0 | 23 || 24 | 1 | IN | CE0 | 10 | 8 | | | | 0v | | | 25 || 26 | 1 | IN | CE1 | 11 | 7 | | 0 | 30 | SDA.0 | IN | 1 | 27 || 28 | 1 | IN | SCL.0 | 31 | 1 | | 5 | 21 | GPIO.21 | IN | 1 | 29 || 30 | | | 0v | | | | 6 | 22 | GPIO.22 | IN | 1 | 31 || 32 | 0 | IN | GPIO.26 | 26 | 12 | | 13 | 23 | GPIO.23 | IN | 0 | 33 || 34 | | | 0v | | | | 19 | 24 | GPIO.24 | IN | 0 | 35 || 36 | 0 | IN | GPIO.27 | 27 | 16 | | 26 | 25 | GPIO.25 | IN | 0 | 37 || 38 | 0 | IN | GPIO.28 | 28 | 20 | | | | 0v | | | 39 || 40 | 0 | IN | GPIO.29 | 29 | 21 | +-----+-----+---------+------+---+----++----+---+------+---------+-----+-----+ | BCM | wPi | Name | Mode | V | Physical | V | Mode | Name | wPi | BCM | +-----+-----+---------+------+---+---Pi 2---+---+------+---------+-----+-----+ */ #define LED_A 25 //BCM：26 #define LED_B 24 //BCM：19 #define LED_C 23 //BCM：13 #define LED_D 22 //BCM：6 #define LED_E 21 //BCM：5 #define LED_F 14 //BCM：11 #define LED_G 13 //BCM：9 #define LED_DP 12 //BCM：10 // 定义1到4号数码管阳极对应的GPIO口 #define DIGIT1 26 //BCM：12 #define DIGIT2 27 //BCM：16 #define DIGIT3 28 //BCM：20 #define DIGIT4 29 //BCM：21 // 定义按钮输入的GPIO口 #define btn 2 //BCM：27 #define FALSE 0 #define TRUE 1 #define t 5000 //usleep延时长度（单位um微秒，1000um＝1ms，1000ms＝1s） // 指定no(1-4)号数码管显示数字num(0-9)，第三个参数是显示不显示小数点（1/0） void showDigit(int no, int num, int showDotPoint); time_t now; struct tm *tm_now; int main (void) { wiringPiSetup () ; pinMode (LED_A, OUTPUT) ; pinMode (LED_B, OUTPUT) ; pinMode (LED_C, OUTPUT) ; pinMode (LED_D, OUTPUT) ; pinMode (LED_E, OUTPUT) ; pinMode (LED_F, OUTPUT) ; pinMode (LED_G, OUTPUT) ; pinMode (LED_DP, OUTPUT) ; pinMode (DIGIT1, OUTPUT) ; pinMode (DIGIT2, OUTPUT) ; pinMode (DIGIT3, OUTPUT) ; pinMode (DIGIT4, OUTPUT) ; pinMode (btn, INPUT) ; pullUpDnControl (btn, PUD_UP) ; digitalWrite (DIGIT1, HIGH) ; digitalWrite (DIGIT2, HIGH) ; digitalWrite (DIGIT3, HIGH) ; digitalWrite (DIGIT4, HIGH) ; for (; ; ) { time(&now); tm_now=localtime(&now); // 按钮按下时显示日期，否则显示时间 // 为了区别左右的数字，让第二个数码管的小数点显示出来 //（本来应该是一个冒号，我们这个数码管没有，就用小数点代替了） if (digitalRead(btn) == HIGH) { usleep(t); showDigit(1, tm_now->tm_hour / 10, FALSE); usleep(t); showDigit(2, tm_now->tm_hour % 10, TRUE); usleep(t); showDigit(3, tm_now->tm_min / 10, FALSE); usleep(t); showDigit(4, tm_now->tm_min % 10, FALSE); } else { // 取得的月份和日期都是从0开始的，所以显示前需要加1 usleep(t); showDigit(1, (tm_now->tm_mon+1) / 10, FALSE); usleep(t); showDigit(2, (tm_now->tm_mon+1) % 10, TRUE); usleep(t); showDigit(3, (tm_now->tm_mday+1) / 10, FALSE); usleep(t); showDigit(4, (tm_now->tm_mday+1) % 10, FALSE); } } return 0 ; } void showDigit(int no, int num, int showDotPoint) { // 先将正极拉低，关掉显示 digitalWrite (DIGIT1, LOW) ; digitalWrite (DIGIT2, LOW) ; digitalWrite (DIGIT3, LOW) ; digitalWrite (DIGIT4, LOW) ; if (num == 0) { digitalWrite (LED_A, LOW) ; digitalWrite (LED_B, LOW) ; digitalWrite (LED_C, LOW) ; digitalWrite (LED_D, LOW) ; digitalWrite (LED_E, LOW) ; digitalWrite (LED_F, LOW) ; digitalWrite (LED_G, HIGH) ; } else if (num == 1) { digitalWrite (LED_A, HIGH) ; digitalWrite (LED_B, LOW) ; digitalWrite (LED_C, LOW) ; digitalWrite (LED_D, HIGH) ; digitalWrite (LED_E, HIGH) ; digitalWrite (LED_F, HIGH) ; digitalWrite (LED_G, HIGH) ; } else if (num == 2) { digitalWrite (LED_A, LOW) ; digitalWrite (LED_B, LOW) ; digitalWrite (LED_C, HIGH) ; digitalWrite (LED_D, LOW) ; digitalWrite (LED_E, LOW) ; digitalWrite (LED_F, HIGH) ; digitalWrite (LED_G, LOW) ; } else if (num == 3) { digitalWrite (LED_A, LOW) ; digitalWrite (LED_B, LOW) ; digitalWrite (LED_C, LOW) ; digitalWrite (LED_D, LOW) ; digitalWrite (LED_E, HIGH) ; digitalWrite (LED_F, HIGH) ; digitalWrite (LED_G, LOW) ; } else if (num == 4) { digitalWrite (LED_A, HIGH) ; digitalWrite (LED_B, LOW) ; digitalWrite (LED_C, LOW) ; digitalWrite (LED_D, HIGH) ; digitalWrite (LED_E, HIGH) ; digitalWrite (LED_F, LOW) ; digitalWrite (LED_G, LOW) ; } else if (num == 5) { digitalWrite (LED_A, LOW) ; digitalWrite (LED_B, HIGH) ; digitalWrite (LED_C, LOW) ; digitalWrite (LED_D, LOW) ; digitalWrite (LED_E, HIGH) ; digitalWrite (LED_F, LOW) ; digitalWrite (LED_G, LOW) ; } else if (num == 6) { digitalWrite (LED_A, LOW) ; digitalWrite (LED_B, HIGH) ; digitalWrite (LED_C, LOW) ; digitalWrite (LED_D, LOW) ; digitalWrite (LED_E, LOW) ; digitalWrite (LED_F, LOW) ; digitalWrite (LED_G, LOW) ; } else if (num == 7) { digitalWrite (LED_A, LOW) ; digitalWrite (LED_B, LOW) ; digitalWrite (LED_C, LOW) ; digitalWrite (LED_D, HIGH) ; digitalWrite (LED_E, HIGH) ; digitalWrite (LED_F, HIGH) ; digitalWrite (LED_G, HIGH) ; } else if (num == 8) { digitalWrite (LED_A, LOW) ; digitalWrite (LED_B, LOW) ; digitalWrite (LED_C, LOW) ; digitalWrite (LED_D, LOW) ; digitalWrite (LED_E, LOW) ; digitalWrite (LED_F, LOW) ; digitalWrite (LED_G, LOW) ; } else if (num == 9) { digitalWrite (LED_A, LOW) ; digitalWrite (LED_B, LOW) ; digitalWrite (LED_C, LOW) ; digitalWrite (LED_D, LOW) ; digitalWrite (LED_E, HIGH) ; digitalWrite (LED_F, LOW) ; digitalWrite (LED_G, LOW) ; } if (showDotPoint == 1) { digitalWrite (LED_DP, LOW) ; } else { digitalWrite (LED_DP, HIGH) ; } if (no == 1) { digitalWrite (DIGIT1, HIGH) ; } else if (no == 2) { digitalWrite (DIGIT2, HIGH) ; } else if (no == 3) { digitalWrite (DIGIT3, HIGH) ; } else if (no == 4) { digitalWrite (DIGIT4, HIGH) ; } }c的wiringPi库的安装和代码的编译执行方法请自行百度。c语言测试结果是显示稳定了很多，但仍然有一点不稳定不仔细观察基本看不出来了。这个应该跟我代码的效率有关，逻辑应该还可以精简节省cpu资源。这个以后再试了。我们这个系列主要是让大家了解各种基本外设传感器的原理和使用方法。用c语言也只是做一点延伸，以后的教程还是以python为主。源代码下载点击下载Python源码点击下载c源码系列文章树莓派GPIO入门01-使用GPIO接口控制发光二极管闪烁树莓派GPIO入门02-GPIO控制LED亮度，制作呼吸灯效果树莓派GPIO入门03-GPIO控制RGB彩色LED灯树莓派GPIO入门04-使用按钮树莓派GPIO入门05-驱动数码管显示数字【当前文章】树莓派GPIO入门06-跟数字湿温度计DHT11通信树莓派GPIO入门07-利用声音传感器制作声控灯树莓派GPIO入门08-使用74HC595芯片驱动数码管（一）树莓派GPIO入门08-使用74HC595芯片驱动数码管（二）树莓派GPIO入门09-使用MAX7219芯片驱动8位数码管树莓派GPIO入门10-使用TLC5940芯片输出多路PWM树莓派GPIO入门11-驱动液晶屏幕（一）树莓派GPIO入门11-驱动液晶屏幕（二）