自制NixieDisplay辉光数码管显示时钟

本文介绍了一种使用辉光管制作多功能时钟的过程,包括电路设计、Arduino编程及外壳制作等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    最近偶然发现一种发光效果非常独特的数码管,非常感兴趣,于是查阅了一些资料,原来是电子管时代用于数字和符号显示的辉光数码管(nixie tube)。与现在常见的LED驱动数码管不同,辉光管的是由金属符号字形的阴极、高压栅网状阳极以及其中填充的惰性气体组成的,当阳极通入高压时,会使得阴极向惰性气体放电,导致惰性气体发光。第一次看到辉光管的发光效果时,我以为是其阴极类似以前的白炽灯泡灯丝一样通大电流发光的,但实际上辉光管的阴极本身是不发光的,真正发光的是“”悬浮”在阴极上方的一层惰性气体在发光。后来测试过程中发现实际阴极的电流也很小,只有2mA的量级,因此还是很适合用于时钟显示的。

    了解了基本原理后,我就立刻在淘宝上买了两个国产的辉光管回来测试,由于年代久远,基本世界上辉光电子管均已停产,能买到的都是二手产品。国产辉光电子管相较于国外型号价格要低很多,同时货源也更多,常见的一款型号是QS30-1,单个管子可显示0-9十个数字,阳极起辉电压170V。我买的这款是南昌电子厂生产的,只要11块一个。当送过来刚拿到手的时候,感觉比我想象中要小一点,也比图片中显示的要精致很多,透明的圆玻璃外壳里面是细密的十个重叠着的数字形状的阴极丝,上面有一层六角形网栅状阳极,玻璃外壳下面圆周排列13个引脚,其中有两个是这个管子用不到的。在玻璃外壳的侧面,印着“南昌 QS30-1”,下方是生产日期,字已经有些模糊不清,但还是能辨认出,分别是1977年12月和1982年7月,这可是比我的年龄还大很多,想着这两个管子也许曾经在某个大型计算机上默默发着光,被我们国家那些年代的科学家们注视着,就似乎有种恍如隔世的感觉。



辉光管点亮测试

   因为辉光管是需要170V高压驱动的,为保险起见,在制作之前先单独测试能否成功点亮。使用一个成品5V转170V高压模块,其实从价格和转换效率来说用12V转170V的模块要好很多,但考虑到通用性,还是用了较贵的5V转换模块。170V模块开路实测电压183V,基本满足要求。在阳极串联了20k欧姆的限流电阻后练到辉光管中,接通电源后成功亮了起来,这时测了一下阴极电流,两个管子分别是2.7mA和2.2mA,改变限流电阻为70k欧姆时,电流降到0.7mA,此时辉光管亮度只是稍微暗了一点。阴极开路电压测得为50V,改变限流电阻开路电压无变化。同时发现,只要有一个阴极导通,其余所有阴电压均降到0。

  在网上查到很多国内外爱好者也设计了很多辉光管显示的时钟,但为了完整显示一般至少都要6个管子,我想做一点不一样的,同时出于设计结构最简洁、功能最丰富的原则,我设计了一个方案,只用两个管子就可以具备显示温度、湿度、时间切换显示、小时显示、分钟显示、秒钟的6个功能,通过旋钮或者按键切换(好了,我承认当然其实还有一个重要原因是买齐6个管子和附带的所有驱动电路元件所需要的钱实在有点多。。。你可以叫我穷人的方案)按一定间隔切换显示时、分、秒就可以完整显示出当前时间,温湿度均是两位十进制数直接显示,同时加一个led灯用于指示温度处于零下。这样就初步构想了整体方案。


辉光管点亮成功

 arduino主控MCU

   主控制MCU这里选择用了arduino pro mini,这其实就是一个基于atmel的ATmege328P单片机的最小系统板,体积做的很小,同时具有丰富的片上资源,有32K的FLASH,8K SRAM和1K的EEPROM,此外还有数个10bit AD通道,数个PWM输出端口,以及SPI和I2C接口等,而且我买的价格只要不到10块钱一个,很适合用来做这样的一些小制作。编译环境采用的是arduino IDE,arduino是一个越来越流行的开源硬件项目,源自意大利,最早支持AVR系列单片机,但现在支持的产品线已经扩展到32位的Soc了。具体的大家可以登录其官网arduino.cc查阅。

 功能切换方案(电位器ad采样+滞回滤波)

  因为一共设计了6个功能相互切换,采用按键的方式,一个按键对应一个功能是最容易想到的方法,但这一是太占用单片机的IO口,另外需要考虑同时按下多个按键之间的互相干扰问题,如果加入专门的按键检测硬件电路感觉又增加了成本,也没太多必要,看起来也比较臃肿。于是我采用了电位器AD采样切换的方案,这样一是成本很低,只需要一个电位器,同时更加简洁,外观上只有一个旋钮。而且最多的好处是可以任意扩展或减少需要切换的功能,只需要在程序中修改就可以了(没错,我就是又想省钱又想效果好:~),具体也很简单,就是测量电位器上移动脚的电压值,在程序中做判断是否为预先设计好的范围内即可,为了提示出现在处于什么功能状态,另外需要增加几个led灯,每切换到一个功能就将其对应的led点亮。这里要注意选择此种方法时,因为AD采样的数值会有抖动,所以需要加入滞回滤波算法以防止出现旋钮在某个位置时两个相邻状态不停来回切换的情况。滞回滤波算法的作用和运放正反馈构成的滞回比较器很相似,这里贴了一张网上找的硬件滞回比较器的电路图帮助大家回忆一下,算法实现起来也不复杂,简单来说就是设置一个不定义的区间即可,同时需要设定好抖动区间。在尝试了几次后在我的程序中将抖动容忍区间AD_SHAKE设定为10时就有很好的效果,再也没有出现两个状态指示灯来回切换闪烁的情况,具体代码如下。


#define AD_SHAKE 10   

void led_state()
{
  led_Value = analogRead(led_ad);  // read the voltage from the potentionmeter(0-1023)
  if (led_Value > (1000 + AD_SHAKE) && led_Value < 1023)
  { led_display(~(1 << LED_TEM));
    state = Tem;
  }
  else if (led_Value > (900 + AD_SHAKE) && led_Value < (1000 - AD_SHAKE))
  { led_display(~(1 << LED_HUM));
    state = Hum;

  //else if...........

 }


   温湿度模块和时钟模块测试

  温湿度传感器选用的是AM2320,温湿度采样值均为16bit长度,时钟芯片使用的是PCF8563,这款芯片可以用于显示年月日星期时分秒,自动计算大小月和闰月,还可以产生几个固定频率的方波,不过这次制作用不到频率输出功能。这两款芯片均支持标准I2C总线通信,这样只需要占用单片机的两个IO口,对于这次用的mega328P而言,少占用IO口还是很有意义的。我在正式搭建电路和写整体程序前,先在面包板上搭建了一个测试电路,然后写一些单独的测试程序用于逐个模块的调试,这样最后的工作量就会小很多,同时错误也会被提前发现。测试这两个传感器时,我写程序设定每隔十分钟测量一次温湿度并将数值和对应的时间记录到片上自带的EEPROM中,然后放在实验室里,第二天过来打开数据就看到了实验室一个昼夜的详细温湿度变化了。

第二天来提取实验室里采集的温湿度数据



温湿度和时钟模块的测试程序

//
// AM2320 & PCF8563 test code
//
//stroe the temperature&humidity every 10 minutes,and record the time
//
#include <Wire.h>
#include <AM2321.h>
#include <Rtc_Pcf8563.h>
#include"Timer.h"
#include<EEPROM.h>

Rtc_Pcf8563 rtc;
Timer t;
AM2321 ac;
#define _10MIN_  600000L
int eeAddr = 0;
int eeAddr_get = 2;

const int button_get = 31; //when press show the eeprom
const int button_init =33;//initial the address of reading EEPROM
byte buttonState =1;
byte initiState =1;

struct RT_Sensor{
  byte day;
  byte hour;
  byte minute;
  byte second;
  int temperature;
  int humidity;
  }tem_hum;

void setup() {
  Serial.begin(9600);
  pinMode(button_get, INPUT);
  pinMode(button_init, INPUT);
//  storeEEPROM();
  
//  EEPROM_addr_init();     // only should run just one time ,like the RTC initial
//  t.every(2000,storeEEPROM, 10);
  t.every(_10MIN_, storeEEPROM);
//  t.after(10000, getEEPROM_10);
//  getEEPROM(50);
}
void getEEPROM_all()
{
  eeAddr = 0;
  for(int i =0;i<(EEPROM.length()/sizeof(RT_Sensor)); i++)
    getEEPROM();
  }
void getEEPROM(int num)
{
//  eeAddr_get = 2;
  for(int i=0;i<num;i++)
    getEEPROM();
  }
void getEEPROM()
{
  EEPROM.get(eeAddr_get, tem_hum);
  eeAddr_get += sizeof(RT_Sensor);
  Serial.print("Day: ");
  Serial.print(tem_hum.day);
  Serial.print("  ");
  Serial.print("Time: ");
  Serial.print(tem_hum.hour);
  Serial.print(':');
  Serial.print(tem_hum.minute);
  Serial.print(':');
  Serial.print(tem_hum.second);
  Serial.print("  ");
  Serial.print("Temperature: ");
  Serial.print(tem_hum.temperature/10.0);
  Serial.print(" C  ");
  Serial.print("Humidity: ");
  Serial.print(tem_hum.humidity/10.0);
  Serial.println(" %");
  }
void EEPROM_addr_init()
{
  EEPROM.put(0, 2); //write EEPROM from the third unit,the first two unit used to store addr
  }
void storeEEPROM()
{
  EEPROM.get(0, eeAddr);     //little-End mode
  tem_hum.day = rtc.getDay();
  tem_hum.hour = rtc.getHour();
  tem_hum.minute = rtc.getMinute();
  tem_hum.second = rtc.getSecond();
  ac.read();
  tem_hum.temperature = ac.temperature;
  tem_hum.humidity = ac.humidity;

  EEPROM.put(eeAddr, tem_hum);
  eeAddr += sizeof(RT_Sensor);
  if(eeAddr >=EEPROM.length())
    eeAddr = 2;
  EEPROM.put(0, eeAddr);  //put the current address to the first two byte of EEPROM
  }
void temper_humid()
{
   Serial.print("(");
  Serial.print(ac.read()?"OK,":"Wrong,");
  Serial.print("Time = ");
  Serial.print(rtc.formatTime());
  Serial.print(",");
  Serial.print("Tempetrue = ");
  Serial.print(ac.temperature/10.0);
  Serial.print(" C");
  Serial.print(",");
  Serial.print("Humidity = ");
  Serial.print(ac.humidity/10.0);
   Serial.print(" %");
  Serial.println(")");
  }

void dete_button()
{
  buttonState = digitalRead(button_get);
  if(buttonState == 0)
    {
      getEEPROM(30);
      buttonState = 1;
      Serial.println();
      }
   initiState = digitalRead(button_init);  
   if(initiState == 0)
      eeAddr_get = 2;
      
   
  }

void loop() {
  t.update();
  dete_button();
}


  辉光管驱动电路 74595+K155ID1

    辉光管的驱动本身很简单,十个引脚分别对应0-9,要显示哪个数字就把对应引脚导通即可,只是单片机上没有那么多IO做不到每个数字分配一个引脚,于是使用了两个HC74595移位寄存器,这样两个管子只需要3个引脚即可。同时由于辉光管的阴极关断电压高达50V,因此不能直接将阴极连到595上,中间需要加一个高压驱动电路。高压驱动电路从成本上用高压三极管是最低的,但由于自己没有画PCB板,用三极管驱动的话,连线太多,手焊太麻烦,体积也太大。这个问题在准备制作之前困扰了我很长时间,直到我偶然发现了有一种高压驱动芯片专用于辉光管的阴极驱动,这样问题就迎刃而解了,这种专用驱动芯片型号不多,主要的是美国的SN74141N,日本也有类似型号生产。不过我无意中发现俄罗斯也有一款用于驱动辉光管的耐高压BCD解码驱动芯片K155ID1(原型号字母为俄文),使用起来和74141一样,而且最近还在生产。而同时我还发现一张照片,上面显示我们北方的邻居俄罗斯居然在最近几年举办的坦克大赛里还用辉光管作为数字显示比赛得分!!!呵呵,战斗民族还挺怀旧。

前几年俄罗斯坦克大赛用的辉光管计数器


外壳制作

    各个模块的程序都测试通过后,我就着手开始制作一个盒子来装这个小设计,一开始是想用木头盒子,但是手边找不到合适改造的闲置木头盒子,也没有专门加工木头的工具,只得作罢。后来看到之前一直没扔的手机盒子,发现虽然是纸做的,但还比较结实规整,也好加工,于是就选定它了。


    一开始我是想简单地直接在盒子打孔然后把辉光管底座和电位器等用螺钉拧上去,但后来发现纸盒子毕竟比较松散,切出来的圆孔边缘很容易凹陷下去,所以为了保证辉光管的稳固,在盒子正面加贴了一块废弃塑料校园卡(感谢胡同学和张同学的友情赞助~)作为补强,最终完成后的稳定度还是令人满意的。这里的问题主要是如何在塑料板上打出规整的圆孔,我采用的是用小手钻内缘等距钻孔,然后再用锉刀和砂纸打磨规整,这一步花了不少时间:


圆周打孔


打磨规整,恰好可以卡住底座又不让其掉下去


    然后因为盒子可能会经常会被打开,为了方便焊接,也为了防止完成后无意中打开盒子把盒盖和盒底之间的连线扯断,我加了三组排线插座用于上下两面板的连接,此外的制作过程也没什么可说的了,主要都是一些比较繁琐的焊接打孔适配固定等等。这里插上一张完成后的内部结构图。


最后完成的效果就是这样的:


最后补上最终的完整程序代码,因为我之后想有机会把代码上传到github上,为了方便交流,我自己的程序都开始尽量使用英文注释(要是有语法错误还请大家指出来啊~)

#include <Wire.h>
#include <AM2321.h>
#include <Rtc_Pcf8563.h>
#include"Timer.h"
#include<avr/wdt.h>     //the nix clock  frequently hangs without finding out why,so add a watch dog to fix it

#define SCK_NIX 9
#define SI_NIX  7
#define RCK_NIX 8

#define SCK_LED 6
#define SI_LED  4
#define RCK_LED 5

#define LED_TEM 7
#define LED_HUM 6
#define LED_TIM 5
#define LED_YEA 4
#define LED_MON 3
#define LED_DAT 2
#define LED_NEG 1

#define AD_SHAKE 10

#define _1MIN_ 60000L
#define _10MIN_ 600000L

Rtc_Pcf8563 rtc;
Timer t;
AM2321 ac;

int led_ad = A0;
int led_Value;

enum funcState {Tem, Hum, Tim, Wek, Mon, Dat } state;
void nix_led_init()
{
  pinMode(SCK_LED, OUTPUT);
  pinMode(SI_LED, OUTPUT);
  pinMode(RCK_LED, OUTPUT);

  pinMode(SCK_NIX, OUTPUT);
  pinMode(SI_NIX, OUTPUT);
  pinMode(RCK_NIX, OUTPUT);

}
void led_display(byte ledcode)
{
  shiftOut(SI_LED, SCK_LED, MSBFIRST, ledcode);
  //send the data from shift register to output latch
  delayMicroseconds(5);
  digitalWrite(RCK_LED, HIGH);
  delayMicroseconds(5);
  digitalWrite(RCK_LED, LOW);
}
void nix_display(int num)
{
  byte nix_dig = (abs(num) % 10) | (abs(num) / 10 % 10 << 4); //turn the hex to bcd
  shiftOut(SI_NIX, SCK_NIX, MSBFIRST, nix_dig);
  delayMicroseconds(5);
  digitalWrite(RCK_NIX, HIGH);
  delayMicroseconds(5);
  digitalWrite(RCK_NIX, LOW);
}

void nix_off(int num)
{
  shiftOut(SI_NIX, SCK_NIX, MSBFIRST, 0xff);
  delayMicroseconds(5);
  digitalWrite(RCK_NIX, HIGH);
  delayMicroseconds(5);
  digitalWrite(RCK_NIX, LOW);
  delay(num);
}
void led_state()
{
  led_Value = analogRead(led_ad);
  if (led_Value > (1000 + AD_SHAKE) && led_Value < 1023)
  { led_display(~(1 << LED_TEM));
    state = Tem;
  }
  else if (led_Value > (900 + AD_SHAKE) && led_Value < (1000 - AD_SHAKE))
  { led_display(~(1 << LED_HUM));
    state = Hum;
  }
  else if (led_Value > (740 + AD_SHAKE) && led_Value < (900 - AD_SHAKE))
  { led_display(~(1 << LED_TIM));
    state = Tim;
  }
  else if (led_Value > (515 + AD_SHAKE) && led_Value < (740 - AD_SHAKE))
  { led_display(~(1 << LED_YEA));
    state = Wek;
  }
  else if (led_Value > (279 + AD_SHAKE) && led_Value < (515 - AD_SHAKE))
  { led_display(~(1 << LED_MON));
    state = Mon;
  }
  else if (led_Value > 0 && led_Value < (279 - AD_SHAKE))
  { led_display(~(1 << LED_DAT));
    state = Dat;
  }
}

void tem_display()
{
  ac.read();
  int tem = ac.temperature / 10;
  nix_display(tem);
  if (tem < 0)
    led_display(~(1 << LED_TEM | 1 << LED_NEG)); //negative temperature indicate (will case the negative led blink because of the "ledstate()" just light the tem_led)
}

void hum_display()
{
  ac.read();
  nix_display(ac.humidity / 10);
}

void tim_display()
{

  static int num_h = 0;   //use to dislay several second time by record the how many time the function has been load
  static int num_m = 0;
  static int num_s = 0;
  if (num_h < 10 && num_m == 0 && num_s == 0)
  { nix_display(rtc.getHour());
    delay(200);
    num_h++;
  }
  else if (num_h == 10 && num_m < 10 && num_s == 0)
  {
    if (num_m == 0 )
      nix_off(200);;
    nix_display(rtc.getMinute());
    delay(200);
    num_m++;
  }
  else if (num_h == 10 && num_m == 10 && num_s < 40)
  {
    if (num_s == 0)
      nix_off(200);;
    nix_display(rtc.getSecond());
    delay(200);
    num_s++;
  }
  else if (num_s == 40)
  {
    nix_off(200);
    num_h = 0, num_m = 0, num_s = 0;

  }

}
void wek_display()
{
  nix_display(rtc.getWeekday());
}
void mon_display()
{
  nix_display(rtc.getMonth());
}
void dat_display()
{
  nix_display(rtc.getDay());
}

void welcome()
{
  int n = 1;
  for (int i = 0 ; i < 10; i++)
  {
    wdt_reset();
    nix_display(11 * i);
    led_display(~(1 << n++));
    if (n >= 8)
      n = 1;
    delay(400);
  }
  delay(400);
  for (int i = 9 ; i >= 0; i--)
  {
    wdt_reset();
    nix_display(11 * i);
    led_display(~(1 << n++));
    if (n >= 8)
      n = 1;
    delay(400);
  }

}
void setup() {
  wdt_disable();      //  must disable the WDT at first or it may always reboot and could enter in the loop
  //
  Serial.begin(9600);
  nix_led_init();
  welcome();        //the welcome function using more than 4 second.so must reset the WDT inside it !!!
  t.every(_10MIN_, welcome);  //aviod the cathode poisoning

  //the WDT time can be choose from
  //WDTO_25MS  WDTO_30MS  WDTO_60MS  WDTO_250MS  WDTO_500MS
  //WDTO_1S  WDTO_3S  WDTO_4S  WDTO_8S
  //be very carfully when use too short time value because  most avr's WDT still on work even after reboot
  wdt_enable(WDTO_4S);      //had better enable it after initial case some initial function is time-consuming
}
void loop() {
  t.update();
  wdt_reset();    //reset the WDT every loop,or it will cause reboot
  led_state();
  switch (state)
  {
    case Tem: tem_display(); break;
    case Hum: hum_display(); break;
    case Tim: tim_display(); break;
    case Wek: wek_display(); break;
    case Mon: mon_display(); break;
    case Dat: dat_display(); break;
    default: tem_display(); break;
  }
}


    这是我的第一篇CSDN博客,以一个小制作开头,还不知道怎么上传代码链接,只能整段粘过来,版面和文字上也有一些的问题。不过希望可以对这个小制作感兴趣的朋友们提供一些帮助,同时也希望借这个平台记录自己的学习历程,和大家多多交流相互提高。有关于这个制作的电路或者程序方面任何问题,都欢迎在评论里留言或者发送到我的邮箱 xcchengl@126.com,我会很乐意解答。



【舸轮综合船舶工作室】出品 欢迎关注b站up主:舸轮综合船舶制造 查看更多资源及教程 不保证没错误,本工作室不为使用此套开源资料造成的任何后果负责! IN14辉光钟 PCB文件及程序源码说明 版本V1.1 主要是对我奇怪的电路设计风格做一个解释(╯‵□′)╯︵┻━┻ 除了右边有一个8550外,PCB中几乎所有的三极管型号均为13001 7805最好加一个微型的散热器,实测发热较大 NE555只是拿来闪烁冒号的,不是升压的,需另外配升压板 NE555右上方那个R 500k阻值具体是多少需要试,这个阻值决定了冒号的闪烁频率 闪烁的冒号(氖泡)从板子左下角的两个2pin分别接入,切记不可并联后接入 PowerRealy是一个继电器,是用来控制升压模块通断的,封装是典型黄色的HK信号继电器 继电器左边的两个接口,标有- S +的是红外热释探头的接入口,实现人来自动开,可在-和S之间再并联一个自锁开关可实现手动开关,另一个是-IN+是电源输入,参考电压12v,电流约0.2A 单片机左边的2pin接口是升压模块电源接口 调时按钮是KEY1 KEY2 R4 R2 R7 R1是四个8路排阻,排阻有小白点的一端对准焊盘正方形的一端 板子下方中间的+HV-是升压模块输出接入点 其左边的Out+分别接4个辉光管的阳极 为了节省板子(偷懒)没有采用常规的74HC573锁存器一组一组扫描着输出,而是采用每个引脚专门控制一位,但引脚刚好又差了一个,无奈就加了一个573,把变化最小的第一位数字和调时按钮接在上面 左上角的2032BAT是接纽扣电池的,能够实现掉电走时,但我不知为何没成功 现在程序还不是很完善,有一些bug,已知的有:在整点时小时位会延时1分钟,比如从20:59到21:00时会先跳到20:00然后一分钟后才会变为21:01,调时时有时候小时那边会一直在十内循环,不管他直接多按几轮一般能出来,调分时有时会只有个位动,好像这时候只能重启了。 焊接时一定要注意三极管引脚之间别短路了! 焊接时一定要注意三极管引脚之间别短路了! 焊接时一定要注意三极管引脚之间别短路了! 即使看着没短路也要用万用表打一下以防万一,Protel自带的三极管封装为什么引脚焊盘之间距离如此感人我也不知道 四组Out-(注意最右边那三个是一组,最左边从Q25和Q26中间引脚引出的两个引脚也是一组)各自接什么参照网上的51单片机引脚定义再对照下表:(左边第一位代表从左往右第几个辉光管,第二位表示此辉光管对应引脚的数字,右边表示单片机的对应引脚) 11 P22 12 P23 20 P34 21 P06 22 P07 23 P21 24 P20 25 P17 26 P30 27 P31 28 P32 29 P33 30 P03 31 P04 32 P02 33 P01 34 P05 35 P00 40 P16 41 P35 42 P36 43 P37 44 P10 45 P11 46 P12 47 P13 48 P14 49 P15
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值