考虑到家里有大量不配对的干电池,每次把万用表带回去测量又不现实,另外买个万用表又有点浪费(其实不贵,但是邮费你可桑得起?),考虑到手头还有闲置的 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; }