北斗星君的专栏

NERVE软件开发组

M8系统编程应用之数字时钟的实现

 


M8系统编程应用之数字时钟的实现

Wayne Huang

2010年6月

0   写在前面的话

自从上一篇文章更新之后,又过了很久。说实话,原本我准备是每周更新一次的。但是,事实却往往事与愿违。之所以隔了那么久,一来是因为工作和学习上刚进入 一个新的环境,所以还没有稳定,再者我自己也需要学习一些新的东西,整理一下曾经的内容。不过,现在回想起来我更新的频率远没有原来那么高了,或许是自己 老了吧。

写了那么多关于软件相关的东西,我也想试着写一些和硬件有关的文章。所以,这次我想先试验一下写一篇看看,如果觉得不好,请各位不要拍砖。如果觉得好,那 还请各位多多支持了。毕竟,你们的支持是我最大的动力。

曾经有一位朋友,看到我说M8的时候问过我,M8是不是某手机的型号。不过很可惜,我这里说的M8是一款由ATMEL生产的一款单片机。至于为什么选这款 单片机而不是选择应用相当广泛的51单片机,我想主要有两个原因。第一,M8能够得到GCC的支持。第二,M8的性能和体系结构比同为ATMEL的 AT89S52要好很多。

1   ATmega8微控制器概述

ATmega8是一款基于AVR RISC体系结构的低功耗8位微控制器。 能实现每MHz百万条指令的执行效率。 从体系结构上讲,其拥有130条指令,且大部分都是单时钟周期的。 此外,还拥有32个8位通用寄存器。最高能在16MHz外部时钟频率下达到16MIPS的执行速度。 在其内部集成了8KB的可编程Flash ROM,用于存储MCU所执行的程序。 其次还拥有1K的内部SRAM。至于其片内功能则也是相当丰富, 如USART 1 、计时计数器、AD转换、SPI通讯接口、I2 C通讯接口 等诸多功能。 其封装形式主要有双列支插和贴片两种形式。作为一款单片机来说,还是相当不错的, 至少同ATMEL的51单片机相比,其优势还是相当高的。 另外,对于最小系统,由于51需要外部晶振和复位电路,因此存在最小系统的概念。 而ATmega8,由于默认情况下采用的是内部上电复位以及片内时钟,所以不存在最小系统的概念,通电便可工作。

2   I/O端口概述

对于一款MCU来说,他与外部交流的主要途径就是通过其I/O口。 所以,在这篇文章中,我就先从I/O端口开始。

2.1   I/O端口物理描述

在ATmega8系列的单片机上,所有的I/O接口都能实现读写功能,而且,引脚之间是互相独立的, 因此,你可以通过 SBI或者CBI这种位操作指令改变某一个引脚的功能而不影响其他引脚。 ATmega8拥3个8位的I/O端口,他们分别被命名为B、C和D。如果你曾经有过使用51的经验。 那么你或许会对51中的上拉电阻记忆犹新,但是在ATmega8中其拥有内部可选的上拉电阻。 也就是说,你可以选择使用内部的上拉电阻或者采用外部上拉电阻。 如果你要选择外部上拉电阻,建议使用20kΩ ∼ 50kΩ

从电气性能上讲,ATmega8的引脚驱动力还是相当强的,而且输出和输入的驱动力是对称的, 所以你不会遇到在51上遇到的输入能力大于输出能力的情况。根据手册上描述, 其驱动电流为20mA。当然在具体的应用时,会存如下几点限制。

  • 所有引脚的电流总和不得超过300mA
  • C0 ∼ C5 引脚的电流总和不能超过100mA
  • B0 ∼ B7、C6、D0 ∼ D7和XTAL2引脚的电流总和不应超过200mA

从手册的指标上讲,一个引脚驱动一个5mA的发光二极管应该是没什么问题的。 而对于7段数码管来说,也不存在51的适合采用共阴极的限制。

2.2   I/O端口相关软件描述

从软件的层面讲,I/O 端口的功能主要通过DDxn、PORTxn这两类寄存器, 以及PUD这个bit位来进行控制。其中"x"为端口名,而"n"则为寄存器位。 各端口寄存器的功能描述,可以见1 。 表中"X"表示的为任意数,即0或1。

DDxn PORTxn PUD I/O 上 拉电阻 说明
0 0 X 输入 高阻态
0 1 0 输入 与外部引脚变化相当
0 1 1 输入 高阻态
1 0 X 输出 输出低电平
1 1 X 输出 输出高电平

Table 1: M8 I/O端口寄存器功能描述

对于对硬件比较陌生的人,可能会不太理解高阻态。其实,高阻态就相当于引脚上接 一个阻值很大的电阻,等同于断开。其中PUD为SFIOR 寄存器中的一位。 同时,M8也能够通过可重复编程的熔丝位设置PUD,一般来说M8的上拉电阻是无效的。

2.3   软件操作范例

下面我们将提供一组M8端口的读写C代码2 。 在这段代码中,我们将从B口读取一个8位数据,送到D口上。





3   Timer0部件概述

计时/计数器是用于限定时间的主要部件。而我们这次要实现的是一个数字时钟。 因此,计时/计数器是我们的核心功能器件。这次我们只涉及到两个计时器的部分功能, 所以让我们继续看计时/计数器吧。

3.1   Timer0物理描述

M8中的Timer0部件是一个8-bit的计时/计数器。其计时/计数功能是独立于MCU的, 也就是说Timer0的工作是不占用MCU的指令空间的。其计时的基准信号能通过软件内部选择。 可以是ClkI/O 分频后的信号,也可以是外部T0引脚的输入信号。当采用分频时, 则Timer0为计时器,当采用T0引脚的输入信号时,就可以作为计数器了。

Timer0相关的计数器寄存器在每次信号到达后,将累加1。当达到最大值0xFF时, TOV0将被置位,如果中断被允许的话,将触发相关的中断程序,并且将TOV0清零。 此时计数器寄存器将从0x00开始重新计数。

3.2   Timer0软件功能描述

Timer0从软件角度看主要通过TCCR0来选择时钟信号,TIMSK中的TOIE0位控制时钟中断, TIFR中的TOV0位判断计数器是否溢出。最后,TCNT0为Timer0的计数寄存器。

CS02 CS01 CS00 说明
0 0 0 无时钟源,计时器停止
0 0 1 采用clkI/O 作为时钟源。
0 1 0 采用clkI/O /8作为时钟源。
0 1 1 采用clkI/O /64作为时钟源。
1 0 0 采用clkI/O /256作为时钟源。
1 0 1 采用clkI/O /1024作为时钟源。
1 1 0 采用T0引脚的下降沿跳变作为时钟源。
1 1 1 采用T0引脚的上升沿跳变作为时钟源。

Table 2: TCCR0 时钟选择寄存器说明

其中TCNT0就直接保存着计数器的值,通过对其赋值便可设定相应的时间。 而TCCR0的低三位为 CS02、CS01和CS00用于选择时钟源,其他位为保留位, 其具体的意义可见2

在具体的使用中,我们经过改变TCNT0的值来等待相应的时间。 假设T为分频基数,其数值为 1、8、64、256或者1024中的一个。 S为等待的秒数。则我们根据原理可以得到等式(1 ) 。 根据要求,将相应的数代入便得到TCNT0的值了。

TCNT0 =255 − clkI/O

T
S
(1)

3.3   软件操作与范例

在这里,我将提供一组C代码。采用中断方式响应计时计数器溢出事件。 这里我们将让计时器每隔约50ms发生一次,并假设时钟频率为1MHz。





4   Timer1部件概述

Timer1相对于Timer0的功能更多,但是在这里我们只使用Timer1中的CTC模式。

4.1   Timer1物理描述

Timer1是一个16bit的计时/计数器。除了拥有Timer0的功能之外, 还拥有PWM模式以及CTC模式等。在计数器读写方面由于M8的数据总线是8位的, 所以Timer1的各个16为寄存器都采用双缓存模式。Timer1的控制主要有 TCNT1、OCR1A/B、ICR1 这些16bit寄存器和一个TCCR1A/B八位寄存器对其控制。

由于我们这里只用到了CTC模式,因此其他具体的硬件描述就暂且省略了。 不过,正因为Timer1是一个16bit的计时/计数器,因此其计数范围远大于Timer0。 Normal模式就和Timer0的功能是一样的,不过主要是计数范围更广。 CTC模式即Clear Timer on Compare Match。主要运用OCR1A或者 ICR1 两个寄存器。 在CTC模式下,当TCNT1的值等于OCR1A或者ICR1时,TCNT1将被清零。

4.2   Timer1软件功能描述

Timer1的功能应该是在M8的3个计时计数器中功能最强大的一个了。 但限于篇幅问题,我这里只简单阐述一下CTC的运用。

根据M8的手册,我们可以看到,当Timer1工作在Mode4和Mode12时,其工作模式为CTC。 其中Mode4时,采用的比较寄存器为OCR1A,而Mode12采用的是ICR1。 除此之外,从软件角度考虑,这两种CTC模式没有太大的区别。

在比较寄存器设置上,鉴于CTC的工作模式,我们可以得到和Timer0类似的公式。 公式 2 中,S 为等待的秒数,T为分频的基数。 将相应的值代入后便可得到OCR1A的值了。

同时,因为CTC模式下,比较数是存储在OCR1A寄存器中的,而在比较结果相同时, TCNT1会自动清零,因此在设置完寄存器后,不用像Timer0那样, 每次都要手动重新设置TCNT中的值。

OCR1A = clkI/O

T
S
(2)

对于时钟频率的选择,Timer1的TCCR1B的第三位同Timer0的TCCR0的第三位一样, 是用于选择时钟频率的。而且,数值也类似,只是在外部平率的选择上, Timer0采用的是T0引脚,而Timer1采用的是T1引脚。除此之外,都类似。

4.3   软件操作与范例

下面的代码也将采用中断响应模式,对计时器比较事件进行响应。 这里,我们将让计时器每个1s发生一次中断事件,并假设时钟频率为1MHz。




5   硬件环境概述

对于一个单片机系统而言,光有单片机是不够的,还需要外部电路的支持。 当然,这个数字时钟从理论上说,也算一个微型系统。因此,单片机外围的硬件 也同样是免不了的。这里的内容主要是针对在硬件方面不太了解的人所设置的, 如果您是硬件牛人,那就跳过啦。

5.1   7段数码管的连接描述

对于一个系统而言,我们都需要有一组交互界面。比如,输出功能能之类的。 在硬件的开发中,比较简单而常见的,就是LED作为输出,即发光二极管。 这个玩意儿其实就是一个具有二极管单向导通性且会发光的器件。 一般来说,一个发光二极管的驱动电流在5mA以上即可。 当然,高出一点也是可以的,根据一般的经验来说, 发光二极管还是非常经得起"折磨"的。

那7段数码管又是个啥呢?就是我们日常生活中,在电梯或者电子秤上看到的那种显示数字 用的那种器件。一般来说,那7个管全部点亮时,显示的是数字8。 不过这里其实有个误导,7段数码管,一般来说有8个LED,最后一个LED就是数字边上的 小数点。从结构上讲,7段数码管就是7个LED拼成一个数字8的形状,再用一个LED 表示一个小数点。

在实际的使用中,为了简化连线和便于使用,常把那一组LED的某个极性的引脚连在一起 作为一个引脚使用。所以,7段数码管常被分为共阴和共阳两种。这两种的区别就在于, 共阴极是把所有LED的阴极连在一起,而共阳极则是把所有LED的阳极连在一起。 一般来说,因为M8的输出电流和输入电流的驱动力是一样的,所以我个人比较喜欢共阴极的。

当然,共极连接也会带来一个问题,那就是LED控制的问题。因为是共阴, 所以,当8个LED全部点亮的时候,其共阴极的电流将至少达到40mA。 这对于单片机来说,如果要控制的话,是一个不小的负担。因此,在使用的时候, 我们会在共阴极上用一个NPN三级管搭一个控制电路,利用小电流来控制三极管的 截止和导通,起到开关的作用。一般来说这个电流不会超过1mA。

5.2   LED的动态刷新法描述

前面,我已经说过了,LED是硬件输出最基本的形式。但是,随之会有另一个问题产生。 单片机的引脚是有限的,而一个7段数码管就要占用8个引脚。而对于一般的系统而言, 一个7段数码管是不够的。就比如这次实现的数字时钟,至少要用4个7段数码管。 如此一来,我们需要至少32个引脚。但是,我们使用的M8是28引脚的。 怎么算也不可能用32个引脚控制啊。

对于这种问题,有两种解决办法。其中比较直接的就是使用外部的寄存器, 将每个脚的数值送到寄存器上锁住,然后驱动数码管。这种方式被称为"静态显示法"。 这种现实方式的优点是便于控制,并且显示方面也比较稳定。缺点么自然很明朗啦, 那就是成本太高了。

另一种现实方式,就是我们标题所提到的,"动态刷新法"。动态刷新法的原理比较简单, 就是利用人眼睛的暂留现象,快速的现实每一位的内容,就和电影的原理差不多。 在器件上来说,同样是4个7段数码管,我们只要用12个引脚就够了。这种方式的优点是 节约了引脚的资源,而且连线也减少了。但缺点是需要一个定时器来控制刷新。

在实践中,有些人会发生利用动态刷新法时,显示的数字会闪烁,或者出现乱码的情况。 其实,出现这种问题的原因,除了是程序编写方面的问题,比如刷新间隔太长,或者 7段数码显示方面的编码问题,也可能是因为刷新方式不对。在使用共阴极数码管进行 动态刷新显示的时候,先要将共阴极置高位熄灭所有LED,然后将信息送到数码管的各 引脚上,最后拉低共阴极的电平点亮指定的数码管。通常,采用这个刷新顺序可以保证 显示的结果相对比较稳定。

6   数字时钟实现概述

在前面那么多篇幅的基础知识准备后,真正描写数字时钟的篇幅却是那么的少。 不过,从另一方面来说,前面的内容其实都是在围绕这个数字时钟的。 因此,这篇文章也不能完全的说是偏题。

6.1   硬件环境的简单描述

通过上面一系列的描述,硬件的链接已经不是啥秘密了。 当然,唯一需要说明的是,为了保证时钟的精确性,我考虑下来准备采用外部 8MHz晶振。使用外部晶振的原因主要是因为,M8内部的振荡电路是RC振荡, 因此随外界温度的变化会有所误差,当然对于这种变化幅度允许比较大的时钟来说, RC振荡也是足够了,毕竟想想我们的父辈们,利用555单稳态电路都能做时钟, 那我们这个近8MHz的RC振荡电路上的一点温度系数的误差又算得了啥? 但是,本着对将时钟从非洲带到北极都能让时钟比较正常工作的目标,选择一个 外部晶振也是可以的。

显示部分,我设计采用的是4个7段数码管进行动态刷新。而输入,则利用M8自带的两个 外部中断来实现。内部的两个时钟Timer0和Timer1。因为Timer1的计数范围更广 而且拥有CTC模式,因此用于秒的计数。而Timer0,则用于动态刷新的计时,因为 动态刷新的定时对时间的误差要求不是很高。

电路图么,其实都是一些简单的连线,也没什么特别的。如果有特别需要,或者有什么 不明白的,可以留言于我。这里就不给出了。

6.2   数字时钟相关的数据结构

在刚开始设计这个玩意儿的时候,我想到的是记录从某一时刻开始到现在的秒数, 也就是像Unix中的那个时间戳那种。但是,由于我们这里用的是单片机,虽然性能很强大, 不过在频繁的高密度的16bit乘除法面前,还是会有些问题的。因此,我最后用 空间换取了时间。C语言描述的数据结构如下。其中status为时钟所处的状态,即比如 是显示时间还是年月等。其他的根据意义都可以理解。至于为什么year要采用16bit, 是因为年的数字远大于255,现在都已经是2000年以后了。




这样,我们计时部分的代码就是简单的加法和比较跳转指令。效率远比将秒数转换成 具体的时间要高,更重要的是,也方便了时钟调整功能的实现。

6.3   动态刷新的代码

在前面,我们已经了解了动态刷新法的刷新过程。接下来,我以动态刷新法写一段代码。 其中,假设B端口连着各7段数码管的那八个引脚上。D端口的低四位接在4个数码管的 共阴极控制电路上。




6.4   计时功能的代码

根据我文章开始的描述,计时功能主要依赖于那个计时模块。没秒钟执行一次 计时函数,更新其中的数值。其代码如下。




7   文外音

这次的文章距上一次间隔比较长,其中有很多原因,很大原因是我个人的因素。 这篇文章主要还是用于尝试写点新的内容,有不足的地方还请各位多多包涵并指出。 至于为什么会开始玩玩硬件么,这个故事就说来话长了。感觉最近发生在身边的感触 都能写成一本书了,我想主要是现在周遭的环境不好吧,有时候觉得,在国内, 技术和人际关系,貌似人际关系会比较重要,技术则可有可无。同时我又觉得, 在国内的技术现状中,真正想为社会做贡献的人才,往往会受到各种奇怪的阻挠, 最后却事与愿违。感觉自己最近貌似牢骚比较多了。

最后还是这句话,如果你要转载此文,请转载时注明出处,以便读者能尽快的得到回复。 如果你们有任何的问题,可以到 http://blog.csdn.net/visioncat 上留下你的疑惑,我会第一时间内回答你的。如需要获取此文的PDF版,也可以留言索取。 最后的最后,我要感谢各位对我一直以来的关注和支持,谢谢你们。

 

Wayne Huang


Footnotes:

1 就 是类似于计算机上的串行口,不过如果要和计算机通讯,需要经过电平转换

2 本 文采用的编译器为GCC


阅读更多
个人分类: 算法/程序论述
想对作者说点什么? 我来说一句

js数字时钟 一个简单的实例

2009年04月09日 3KB 下载

基于verilog的数字钟

2014年05月13日 27.37MB 下载

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭