用 AVR 单片机 DIY 简易电压表

考虑到家里有大量不配对的干电池,每次把万用表带回去测量又不现实,另外买个万用表又有点浪费(其实不贵,但是邮费你可桑得起?),考虑到手头还有闲置的 ATTINY24A 单片机外加四位数码管,所以 DIY 一个也不是不行,于是开始动手。

先奉上电路图。

本着成本最低的原则,数码管的段用单片机的 PA0 – PA6 直接驱动( AVR 的 GPIO 能提供 20mA 的电流,木问题),由于电源是两节干电池(也就是 3V ),而且数码管是以扫描的方式间歇点亮的,所以不加电阻问题不大。

由于手头上四位的数码管是共阳极的,所以位驱动用 S9012 PNP 型 BJT 完成,这个管子可以提供最大 500mA 的电流,拿来点数码管完全足够。位驱动信号由单片机的 PB0 – PB3 输出。至于小数点,如果需要的话可以将数码管的 dp 段通过一个电阻和第一位的位驱动信号相连,这样第一位后面就会总是跟着一个小数点。

由于 ATTINY24A 内建的 ADC 参考电压是 1.1V ,所以这里将输入电压用 47.0kΩ 和 4.70kΩ 的精密电阻分压后由 PA7 输入给单片机内的 10 位 ADC 。这样量程 0.000V – 9.999V ,精度 = (47 + 4.7)/4.7 × 1.1 / 210 = 0.012V ,也就是说数码管最后一位示数实际无意义。同时由于单片机自身大电流驱动数码管会对模拟部分造成影响,所以后面在代码里有了四次采样取平均的超采样做法,消除干扰并尽可能地提升精度。输入电压在 10.000V – 12.100V 时可以显示超量程符号。

至此,单片机的 14 个引脚(包括两个电源脚)全部用完,就连复位脚所在的 PB3 都被当做 IO 给分配出去了。因此烧写程序之前一定要小心检查,否则一旦出错想再 ISP 就比较麻烦了,得保证单片机在编程时上电之前 RST 就已经拉低了。

成品。

由于数码管的引脚比较乱,所以干脆不画板,直接全部飞线。

贴片的 ATTINY24A 要比直插的便宜不少,所以当时就买了贴片的芯片 + 转接板。缺点是体积有点大了。

电源,电池盒自带了开关,这回又可以偷懒了。

位驱动电路的三极管和电阻,貌似其中有一颗有点问题,或者是因为 RST 脚的问题(实际中 PB3 接了第三位,PB2 接了第四位),第三位总是比较暗。

按照设定,开机自检四位全部显示“ 8 ”,然后回零。

测试,效果还行。由于模拟电源和数字电源没分开(毕竟单片机的引脚就那么点),所以有抖动、轻微零偏的话都很正常,一般不会大到影响使用。

最后奉上代码,烧写之后记得把熔丝设置成「 lfuse:0xe2 hfuse:0x5e efuse:0xff 」。

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

#define REFRESH_RATE 500

// Display buffer.
uint8_t dispbuf[4] = {0x40, 0x40, 0x40, 0x40};
// Digit pointer.
uint8_t dispptr = 0;
// NOTICE: Be careful not to pull PA7 high since it is connected to ADC.
// This font is for common-anode segment displays.
uint8_t dispfont[] = {
  0x40, // "0"
  0x79, // "1"
  0x24, // "2"
  0x30, // "3"
  0x19, // "4"
  0x12, // "5"
  0x02, // "6"
  0x78, // "7"
  0x00, // "8"
  0x10, // "9"
  0x06, // "E"
  0x7f, // " "
};

#define BIT(x) (1 << (x))
#define SETBIT(x, y) (x |= BIT(y))
#define CLEARBIT(x, y) (x &= ~ BIT(y))
#define CHECKBIT(x, y) (x & BIT(y))

ISR(TIM1_COMPA_vect) {
  // Inverse all bits since "0" means that the digit is active.
  PORTB = ~(1 << dispptr);   PORTA = dispfont[dispbuf[dispptr]];   dispptr ++;   // Resetting the pointer is the most reasonable way to do it, though may not be the simplest.   if(dispptr == 4) dispptr = 0;   // Light the digit for 1800us then turn off.   _delay_us(1800);   PORTB = 0xff; } void adc_init(void) {   cli();   CLEARBIT(ADCSRA, ADEN);   // Single-end input from channel 7   ADMUX = 7;   // Internal 1.1V reference   CLEARBIT(ADMUX, REFS0);   SETBIT(ADMUX, REFS1);   // Clock / 128 speed   SETBIT(ADCSRA, ADPS0);   SETBIT(ADCSRA, ADPS1);   SETBIT(ADCSRA, ADPS2);   // Enable ADC   SETBIT(ADCSRA, ADEN);   // Reset ADC   CLEARBIT(ADCSRA, ADIE);   CLEARBIT(ADCSRA, ADIF);   CLEARBIT(ADCSRA, ADSC);   sei(); } uint16_t read_mv(void) {   // Start a conversion.   SETBIT(ADCSRA, ADSC);   // Wait for the conversion to complete and convert data in ADC register into mV value.   while(CHECKBIT(ADCSRA, ADIF) == 0);   return ((uint32_t)ADC * (1100 * 11) ) >> 10;
}

void print(uint16_t data) {
  // NOTICE: PB2 and PB3 are in reversed order.
  dispbuf[2] = data / 1000;

  // Report error if voltage exceeded the range.
  if(dispbuf[2] > 9) {
    dispbuf[2] = 10; // "E"
    dispbuf[3] = 11; // " "
    dispbuf[1] = 11; // " "
    dispbuf[0] = 11; // " "
    return;
  }

  // Put every digits into their positions.
  dispbuf[3] = (data % 1000) / 100;
  dispbuf[1] = (data % 100) / 10;
  dispbuf[0] = data % 10;
}

int main(void) {
  uint16_t avg;

  // IO ports setup.
  // PA7 is connected to ADC, configure it as an input pin and no pull-up.
  DDRA = 0x7f;
  DDRB = 0xff;

  // Light up all digits and segments for display testing.
  PORTA = 0x00;
  PORTB = 0x00;

  adc_init();

  // Timer and interrupt setup.
  SETBIT(TCCR1B, CS11);
  SETBIT(TCCR1B, WGM12);
  OCR1A = (uint16_t)(F_CPU / (REFRESH_RATE * 8));

  // Wait 500ms for display testing, then start display refreshing interrupt.
  _delay_ms(500);
  SETBIT(TIMSK1, OCIE1A);

  // Main loop
  while(1) {
    // Oversampling increases accuracy, thus makes last digit meaningful.
    // Delay values are not absolute: there are interrupts.
    _delay_ms(10);
    avg = read_mv();
    _delay_ms(10);
    avg += read_mv();
    _delay_ms(10);
    avg += read_mv();
    _delay_ms(10);
    avg += read_mv();
    print(avg >> 2);
  }
  return 0;
}

发表回复

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

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据