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