树莓派编程(二) — TM1637源码简析

前面成功的使用RPi.GPIO库,设置高低电平完成了LED灯和蜂鸣器的状态控制,因为RPi.GPIO没有提供对TM1637的支持,所以借助了已有的库完成了四位数码管的显示。从tm1637.py源码看,他实际也是使用了RPi.GPIO库。 虽然PRi.GPIO库只提供了简单的设置GPIO高低电平的功能,但是我们前面说过这是所有通信的基础,你可以自己通过设置高低电平来实现所有的协议通信。这么一说和我们只用0和1来编程有点象了。只要有0和1,你可以编写任何代码。而这里有高低电平你可以控制任何设备。所以这一篇打算简单分析一下TM1637的源码。

 

一 TM1637

 

先从高层次的TM1637开始分析。在控制LED和蜂鸣器时,我们使用一个高低电平就能控制设备,因为对于这样的设备来说,他们的通信协议非常简单。 高电平发光、低电平不发光。相比而言,TM1637数码的协议就负责很多。要想知道一个设备的通信协议就需要有设备的资料。

我们在网上买到的TM1637一般是下面这个样子。这是由CATALEX公司出的4-Digit Display。而一般称他为TM1637是指它背后的拿颗20针的控制芯片。整个数码管子是由TM1637芯片(U1)、4位数码管(U2)和4针接口(J1)组成的。

一般购买这个设备时候卖家都会提供原理图。 原理图上给出了原件的定义。

  • U1是数码管,可以看带到4为数码管模块是一个12针接口
  • U2是TM1637芯片,他是20针接口。 和供电电路连接,一共使用了2个电阻,4个电容,这些我们从板子上可以很清楚的看到对应的编号。
  • J1是对外的接口,也就是和我们GPIO连接的针口。

 

数码管

 

4个数码管是COM1~COM4。一共有12个针脚。每个数码管由7段组成,每一段都是一个发光二极管。 结合下面的图可以知道

  • A1-A4 这4个针脚对应了4个数码管
  • A-G 这7个针脚对应了数码管的7段
  • DP针脚对应的是中间的冒号,属于2号数码管

 

TM1637芯片

 

TM1637 是一种带键盘扫描接口的LED(发光二极管显示器)驱动控制专用电路,内部集成有MCU 数字接口、数据锁存器、LED 高压驱动、键盘扫描等电路。本产品性能优良,质量可靠。主要应用于电磁炉、 微波炉及小家电产品的显示屏驱动。采用DIP/SOP20的封装形式。

  • 采用功率CMOS 工艺
  • 显示模式(8 段×6 位),支持共阳数码管输出 键扫描(8×2bit),增强型抗干扰按键识别电路 辉度调节电路(占空比 8 级可调)
  • 两线串行接口(CLK,DIO)
  • 振荡方式:内置RC 振荡(450KHz+5%)
  • 内置上电复位电路
  • 内置自动消隐电路
  • 封装形式:DIP20/SOP20

从TM1637的芯片规格书上可以针脚定义

符号

管脚名称

管脚号

说明

DIO

数据输入/输 出

17

串行数据输入/输出,输入数据在 SLCK 的低电平变化,在 SCLK 的高电平被传输,每传输一个字节芯片内部都将在第 八个时钟下降沿产生一个 ACK

CLK

时钟输入

18

在上升沿输入/输出数据

K1~K2

键扫数据输入

19-20

输入该脚的数据在显示周期结束后被锁存

SG1~SG8

输出(段)

2-9

段输出(也用作键扫描),N 管开漏输出

GRID6~GRID1

输出(位)

10-15

位输出,P 管开漏输出

VDD

逻辑电源

16

5V±10%

GND

逻辑地

1

接系统地

 

还给出了硬件连线图

从图中大致可以看出:

  • TM1637的SG1~SG8针脚和数码管模块的A-G连接 (这个数码管没有小数点所以dp没用)
  • TM1637的GR1~GR6针脚和数码管模块的A1-A4连接 (TM1637支持6位数码管,这里只用到4位
  • TM1637的DIO和CLK是用来和其他设备通信的,最终和我们的树莓派的GPIO口连接

 

简单的分析了一下电路,我们大概可以总结得到:

  • 整个数码管是通过DIO和CLK和外设交互的
  • CLK负责时序控制、DIO负责数据传输
  • 外部设备根据协议传输数据,TM1637芯片根据协议内容,使用GR1~GR6和SG1~SG8这些针脚来控制数码管的显示。

 

 

传输协议

 

然后打算看一下文档上关于传输这一块,好吧,看了半天并没完全弄清协议,作为一个码农压力好大,只能简单总结一下。。。。有兴趣的话可以自己看一下文档:  LED 驱动控制专用电路 TM1637

 

微处理器的数据通过两线总线接口和 TM1637 通信,在输入数据时当 CLK 是高电平时,DIO 上的信号必须保持不变;只有 CLK 上的时钟信号为低电平时,DIO 上的信号才能改变。数据输入的开始条件是 CLK 为高电平时,DIO 由高变低;结束条件是 CLK 为高时,DIO 由低电平变为高电平。

TM1637 的数据传输带有应答信号 ACK,当传输数据正确时,会在第八个时钟的下降沿,芯片内部会 产生一个应答信号 ACK 将 DIO 管脚拉低,在第九个时钟结束之后释放 DIO 口线。

  • 传输有开始开始和结束标记
    • start :CLK 为1, DIO从1变为0
    • stop :CLK为1, DIO从0变为1
  • CLK作为时钟总线,通过高低电平控制时钟频率
  • 传输数据时,DIO数据在CLK高电平变化,CLK低电平时被传输
  • DIO上传输数据正常的话,第8个时钟下降沿会收到ACK, DIO会被拉为低电平

 

 

 

 

从图上大致可以知道,传输协议定义了命令和数据,目前支持3种传输模式。

 

数据指令

 

指令用来设置显示模式和LED 驱动器的状态。在CLK下降沿后由DIO输入的第一个字节作为一条指令。经过译码,取最高B7、B6两位比特位以区别 不同的指令。

B7

B6

指令

0

1

数据命令设置

1

0

显示控制命令设置

1

1

地址命令设置

 

数据命令设置

该指令用来设置数据写和读,B1和B0位不允许设置01或11。

B7

B6

B5

B4

B3

B2

B1

B0

功能

说明

0

1

无关项,填 0

0

0

数据读写模式设置

写数据到显示寄存器

0

1

1

0

读键扫数据

0

1

0

地址增加模式设置

自动地址增加

0

1

1

固定地址

0

1

0

测试模式设置(内
  部使用)

普通模式

0

1

1

测试模式

 

地址命令设设置

B7

B6

B5

B4

B3

B2

B1

B0

显示地址

1

1

无关项,填 0

0

0

0

0

00H

1

1

0

0

0

1

01H

1

1

0

0

1

0

02H

1

1

0

0

1

1

03H

1

1

0

1

0

0

04H

1

1

0

1

0

1

05H

该指令用来设置显示寄存器的地址;如果地址设为0C6H 或更高,数据被忽略,直到有效地址被设定; 上电时,地址默认设为00H。

而地址命令和针脚对应关系如下:

 

显示控制

B7

B6

B5

B4

B3

B2

B1

B0

功能

说明

1

0

无关项,填 0

0

0

0

消光数量设置

设置脉冲宽度为 1/16

1

0

0

0

1

设置脉冲宽度为 2/16

1

0

0

1

0

设置脉冲宽度为 4/16

1

0

0

1

1

设置脉冲宽度为 10/16

1

0

1

0

0

设置脉冲宽度为 11/16

1

0

1

0

1

设置脉冲宽度为 12/16

1

0

1

1

0

设置脉冲宽度为 13/16

1

0

1

1

1

设置脉冲宽度为 14/16

1

0

0

显示开关设置

显示关

1

0

1

显示开

 

 

 

二 TM1637源码

 

 

整个源码很简单,去除测试代码大概只有200行,先回忆一下我们显示一个数到数码管上的的代码

display = TM1637(PIN_CLK, PIN_DIO, 2)
display.Clear()
display.ShowInt(35)

非常的简单。那就先从TM1637的构造函数开始

class TM1637:
    __doublePoint = False
    __Clkpin = 0
    __Datapin = 0
    __brightness = 1.0  # default to max brightness
    __currentData = [0, 0, 0, 0]

    def __init__(self, CLK, DIO, brightness):
        self.__Clkpin = CLK
        self.__Datapin = DIO
        self.__brightness = brightness
        IO.setup(self.__Clkpin, IO.OUT)
        IO.setup(self.__Datapin, IO.OUT)

构造函数很简单,主要就是调用RPi.GPIO来初始化了一下CLK和DIO对应的GPIO接口。并且设置了一下数码管显示的亮度。TM1637类还有2个重要的成员变量,__doublePoint和__currentData,很明显是用来显示数据的。

def Clear(self):
        b = self.__brightness
        point = self.__doublePoint
        self.__brightness = 0
        self.__doublePoint = False
        data = [0x7F, 0x7F, 0x7F, 0x7F]
        self.Show(data)
        # Restore previous settings:
        self.__brightness = b
        self.__doublePoint = point

Clear函数是在初始化之后清空一下数码管的显示。主要是调用了Show方法,传入了一个数组,全部是0x7F。那就看下Show方法

    def Show(self, data):
        #更新成员变量
        for i in range(0, 4):
            self.__currentData[i] = data[i]

        self.start()
        self.writeByte(ADDR_AUTO)
        self.br()
        self.writeByte(STARTADDR)
        for i in range(0, 4):
            self.writeByte(self.coding(data[i]))
        self.br()
        self.writeByte(0x88 + int(self.__brightness))
        self.stop()

Show方法中调用了多个内部方法,从名字看结合前面的协议开始和结束,显示数据到数码管就是数据传输的过程。这里是让数码管不显示任何数据。

    def start(self):
        """send start signal to TM1637"""
        IO.output(self.__Clkpin, IO.HIGH)
        IO.output(self.__Datapin, IO.HIGH)
        IO.output(self.__Datapin, IO.LOW)
        IO.output(self.__Clkpin, IO.LOW)

    def stop(self):
        IO.output(self.__Clkpin, IO.LOW)
        IO.output(self.__Datapin, IO.LOW)
        IO.output(self.__Clkpin, IO.HIGH)
        IO.output(self.__Datapin, IO.HIGH)

看下start和stop方法,其面说过根据文档知道 start (CLK 为1, DIO从1变为0)stop (CLK为1, DIO从0变为1),  从代码实现来看就是这样。 而br方法就是stop后再次调用start

    def br(self):
        """terse break"""
        self.stop()
        self.start()

writeByte是整个代码中的核心部分,用来发送数据,注释中大搞写了发送过程。每发送一个字节会有一个ACK响应。

    def writeByte(self, data):

        # 把一个数据变为8位二进制,根据每一位0或1输出对应的高低电平。
        # 每次发送数据前CLK为0,这样可以设置DIO数据,设置好之后,CLK设置为1,数据被传输。
        for i in range(0, 8):
            IO.output(self.__Clkpin, IO.LOW)
            if(data & 0x01):
                IO.output(self.__Datapin, IO.HIGH)
            else:
                IO.output(self.__Datapin, IO.LOW)
            data = data >> 1
            IO.output(self.__Clkpin, IO.HIGH)

        # 8位数据使用8个时钟周期传输,第8个周期的下降沿会收到ACK应答。
        # 因为收到ACK DIO会变为低电平,所以这里先吧DIO设置为高 (不是很确定)。
        # 然后DIO设置为输入模式
        IO.output(self.__Clkpin, IO.LOW)
        IO.output(self.__Datapin, IO.HIGH)
        IO.output(self.__Clkpin, IO.HIGH)
        IO.setup(self.__Datapin, IO.IN)

        # 等待ACK
        while(IO.input(self.__Datapin)):
            sleep(0.001)
            if(IO.input(self.__Datapin)):
                IO.setup(self.__Datapin, IO.OUT)
                IO.output(self.__Datapin, IO.LOW)
                IO.setup(self.__Datapin, IO.IN)

        # 恢复DIO为输出模式
        IO.setup(self.__Datapin, IO.OUT)

看看前面调用的地方

其面一共调用了4种写数据

ADDR_AUTO = 0x40
ADDR_FIXED = 0x44
STARTADDR = 0xC0

# 从前面知道start后第一个数据是COMMAND,这里是0x40, 二进制是01000000, 
# 从前面表中可以知道这条指令是数据命令设置: 普通模式、自动地址、写数据到显示寄存器
self.writeByte(ADDR_AUTO)

# stop-->start
self.br()

# 同样这个也是一条指令, 0xC0 二进制是11000000, 查表得知指令是: 
# 设置显示地址00H,因为是自动模式,所以后面没写一次数据地址会加1。
self.writeByte(STARTADDR)

# 传入数码管要显示是数据,coding就是把10进制数字转为对应的16进制,其实也就是二进制,
# 表示数码管上7段发光二极管的状态。 因为是自动地址模式,所以按图2的方式传输。
# Clear时写入的是0x7F, coding中会设置为0, 所以就是7段二极管都不显示
for i in range(0, 4):
    self.writeByte(self.coding(data[i]))

# 最后一个指令是0x88 对应10001000,是一条显示控制指令,
# 表示打开显示,脉冲宽度为11/16,这里加上了亮度,应该就是通过脉冲宽度来设置亮度。
self.br()
self.writeByte(0x88 + int(self.__brightness))

清空了数据之后就调用了ShowInt方法来显示数字, 他会把一个数字按位拆分,内部对每个数字循环调用了Show1的方法,表示只显示1位,而不是4位。

    def ShowInt(self, i):
        s = str(i)
        self.Clear()
        for i in range(0, len(s)):
            self.Show1(i, int(s[i]))

整体和Show方法是一样的,有一点区别:

  • 因为只显示1位,所以只传递一次数据, 第一条指令使用了固定地址的方式。
  • 二条指令是给每个数字一个不同的地址,分别是00H~03H对应了GRID1~GRID4,  所以就算只有一位数也是从高位显示,也就是数字是靠左显示,如果想靠右显示就要修改这里。 (因为固定模式所以需要自己每次设置地址)
def Show1(self, DigitNumber, data):
        """show one Digit (number 0...3)"""
        if(DigitNumber < 0 or DigitNumber > 3):
            return  # error

        self.__currentData[DigitNumber] = data

        self.start()
        self.writeByte(ADDR_FIXED)
        self.br()
        self.writeByte(STARTADDR | DigitNumber)
        self.writeByte(self.coding(data))
        self.br()
        self.writeByte(0x88 + int(self.__brightness))
        self.stop()

最后还差一个中间的冒号的显示,这个是通过__doublePoint来控制的。 我们这个模块每个数字后面是没有小数点的,只有中有一个冒号,从前面知道这个冒号是属于第2个数码管。看一下设置它显示代码,其实是吧所有数字都在显示一边。  并没有直接控制。

    def ShowDoublepoint(self, on):
        """Show or hide double point divider"""
        if(self.__doublePoint != on):
            self.__doublePoint = on
            self.Show(self.__currentData)

看一下coding的代码,原来每次设置数据的时候都带上了这个point的 0x80就是10000000, 最高位是控制小数点的。这样写的问题就在于,我必须设置第2个数字才有效,否则无效。比如我ShowInt(1),  这样中间的冒号是不会亮的。所以如果针对这个4位数码管要单独设置冒号亮还是不亮是需要修改一下的。

    def coding(self, data):
        if(self.__doublePoint):
            pointData = 0x80
        else:
            pointData = 0

        if(data == 0x7F):
            data = 0
        else:
            data = HexDigits[data] + pointData
        return data

 

三 总结

 

第一次结合电路图和源码分析了一把。 这其实相当于了解了这个模块的驱动的实现。 当然因为数字电路上很多东西我都不是很清楚,所以可能有很多地方还理解的不清楚。但是整体分析下来让我对时钟信号控制数据有了一些基本的理解。以前对于设备直接间的通信完全在理论支持上,这次终于结合到了实际。 因为这个模块相对比较简单,看着图和代码去分析整体还算好理解,如果是让我自己写那应该还是写不出来的。

 


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注