Как стать автором
Обновить

«Давайте отрежем Сусанину ногу?» (с)

Время на прочтение6 мин
Количество просмотров8.6K
imageПодавляющее большинство работавших с микроконтроллерами однажды оказывались перед сложным выбором:
— Так, на эту ногу ШИМ, на эту — кнопку, на эти — светодиоды… Оп-па… А ноги-то закончились. Без светодиодов некрасиво, без кнопок нефункционально. Придется брать кристалл пожирнее :(
Или другая ситуация — плата с микроконтроллером запрятана с глаз долой, но жгут проводов до индикатора печальной змеей пронзает недра прибора.
Настало время для укрощения этого кабельно-ножечного монстра путем усекновения лишних ног, и поможет нам в этом

АЦП и вот такая простая схемка:

А теперь для заинтересовавшихся вкратце принцип работы.
Основную часть времени схема занимается индикацией. Подав на одну ногу 1, а на другую 0, мы зажигаем соответствующий светодиод. Одна комбинация дает зеленый цвет, вторая красный, а если быстро дергать туда-сюда — получаем оранжевый.
С индикацией разобрались, а теперь о кнопках. Предположим, что на Port2 мы подаем логический 0, а Port1 включаем как вход для встроенного АЦП. При этом благодаря резистору R1 мы получаем некоторый небольшой потенциал на светодиоде. Обратите внимание, что в прямом направлении включен именно зеленый светодиод. Как известно, зеленые (и желтые, кстати, тоже) светодиоды имеют ощутимо большее прямое падение напряжения, чем красные. В нашем случае этого напряжения вполне достаточно, чтобы надежно считывать состояние нажатых кнопок. При нажатии любой из них получается делитель напряжения, и теперь достаточно просто оцифровать полученный сигнал и перекодировать в нажатие кнопок. Кстати, даже если перепутать полярность включения проводов — при правильном подборе делителей кнопки сохранят работоспособность, разве что индикация перепутается. Выбор резисторов R3 и R4 достаточно вольный, главное помнить два правила — меньший резистор не должен мешать зажигать светодиоды, а больший резистор должен давать падение напряжения, достаточное для надежного детектирования. Указанные на схеме номиналы я просто поставил из тех, что под рукой валялись, а потом просто в программе прописал получившиеся напряжения. R2 — токоограничивающий, подбираете исходя из используемых светодиодов и параметров ног микроконтроллера, иногда можно обойтись вообще без него.

А теперь собственно библиотека для работы с этой схемой. В данном конкретном случае в качестве платформы использовалась ATMega48, но я старался писать код максимально железонезависимо, и для перехода на другой кристалл достаточно поменять инициализацию порта и АЦП.
/*
 * biwire.h
 */

#ifndef BIWIRE_H_
#define BIWIRE_H_

// Define connections (PORTC only!)
#define BWPORT1 1
#define BWPORT2 2
// Set pin functions
#define ADCPORT BWPORT1
#define GNDPORT BWPORT2
Если вдруг у вас оказалась одна лишняя нога, то вполне можно задействовать её pull-up вот в этом месте вместо резистора R1

// State for interrupt loop
#define	ADC_SET 0x01
#define	ADC_WAIT 0x02

// LED flashing frequency (in parrots, without small wing)
#define SCAN_FREQ       48
// Work together with SCAN_FREQ to get orange color
#define MAX_COLOR_MIX 50

// Event bits when scan complete
// Main keyboard event indicator - must be cleared after handle
#define BW_EVENT 0x80
// Button pressed
#define BW_EVBT1 0x01
#define BW_EVBT2 0x02
// add additional flags here, like BW_EVBT3=0x04
// EVFIN - event "Indication finished"
#define BW_EVFIN 0x10
// Button released
#define BW_EVOPN 0x20
// If button holds more than BW_LONG_PRESS cycles
#define BW_EVLNG 0x40

// How long we wait for 'LongPress'
#define BW_LONG_PRESS 10

Здесь можно писать приблизительные значения плюс-минус автобусная остановка. Нам главное — задать границы для точного опеределения кнопок
// ADC compare values
#define BW_VBTN1 0x1F
#define BW_VBTN2 0xFE

// Define names for signal array
enum SignalNames {
	LedOff = 0,
	OneGreen = 1,
	OneRed = 2,
	OneOrange = 3,
	TwoGreen = 4,
	TwoRed = 5,
	Alarm = 6,
	OrangeRG = 7
Аналогичным образом добавляете своих сигналов по вкусу
};

// ====== Externals =======
// LED sequence to indicate
extern volatile enum SignalNames BwIndicate;
// Data about button events
extern volatile uint8_t BwButton;
// If not null - delay of led indication, then switch LED off
extern volatile uint8_t BwLedDelay;
// Call on start
void biwire_init(void);

#endif /* BIWIRE_H_ */

Скачать .h
Ну и сам исходник:
/*
 * biwire.c
 */

#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdint.h>
#include "biwire.h"

// Led blink matrix. Only upper 6 bits used for indication loop
static uint8_t bwsignalsR[] = {
		0x00, 0x00, 0xC0, 0xC0,0x00, 0xA0, 0xA0, 0xFC
};
static uint8_t bwsignalsG[] = {
		0x00, 0xC0, 0x00, 0xC0,0xA0, 0x00, 0x50, 0xC0
};

// LED sequence to indicate
volatile enum SignalNames BwIndicate;
// FSM and sequence counter
static volatile uint8_t BwState;
// "LongPress" counter
static volatile uint8_t btn_cnt;
// Data about button events
volatile uint8_t BwButton;
// Orange color created by mixing green and red
static volatile uint8_t color_mix;
// If not null - delay of led indication, then switch LED off
volatile uint8_t BwLedDelay;

void biwire_init(void)
{
     // Timer interrupt
    TCCR0B = _BV(CS00) | _BV(CS01)  | _BV(WGM02); // use CLK/64 prescale value, clear timer/counter on compareA match
    OCR0A = SCAN_FREQ; // preset timer0 OCR byte
    TIMSK0  = _BV(OCIE0A); // enable Output Compare 0 overflow interrupt

    // PIN init
    DDRC |= _BV(BWPORT1) | _BV(BWPORT2);
    // ADC init
    ADCSRA = _BV(ADEN) | _BV(ADPS2); // Activate ADC with Prescaler 16 --> 1Mhz/16 = 62.5kHz
    ADMUX = ADCPORT | _BV(REFS0) | _BV(REFS1) | _BV(ADLAR);  // Select input pin using MUX, left adjust and Internal reference
Internal reference у Атмелей всего 1.1 вольта, что при отсутствии нажатых кнопок дает надежное и непробиваемое 0xFF на выходе АЦП.
    DIDR0 = _BV(ADCPORT); // disable digital input for ADC

    // Set initial state for bw
    BwState = ADC_SET;
    BwIndicate = OneGreen;
    BwButton = BW_EVOPN;
    color_mix = 0;
    BwLedDelay = 0;
}
Таймерное прерывание можете настроить по своему вкусу. Играясь делителями и color_mix можно подбирать скорость мигания и время реакции на нажатие кнопки.
SIGNAL (SIG_OUTPUT_COMPARE0A)    // handler for Output Compare 0 overflow interrupt
{
	if (BwState&ADC_SET) {
		// Prepare to keyscan
		PORTC &= ~_BV(GNDPORT);
		DDRC &= ~_BV(ADCPORT);

		ADCSRA |= _BV(ADSC); //Start conversion
		BwState = ADC_WAIT;
		color_mix = MAX_COLOR_MIX;
		if (BwLedDelay)
			if (!(--BwLedDelay)) {
				BwIndicate = LedOff;
				BwButton |= BW_EVFIN;
			}
	} else {
		if (BwState&ADC_WAIT) {
			// Check if ADC complete
			if (ADCSRA & _BV(ADSC) ) return; // Wait another tick
			PORTB = ADCH;
			if (ADCH > BW_VBTN2) { // Both buttons open
				if (BwButton & (BW_EVBT1|BW_EVBT2)) {
					BwButton = BW_EVENT + BW_EVOPN;
					btn_cnt = 0;
				}
			} else if (ADCH > BW_VBTN1) { // Button 2 pressed
				PORTB ^= 0xff;
				if (BwButton & (BW_EVBT1|BW_EVOPN)) {
					BwButton = BW_EVENT + BW_EVBT2;
					btn_cnt = 1;
				}
Аналогичным образом можно определить диапазон для третьей и всех последующих кнопок, но с точностью срабатывания придется повозиться
			} else { // Button 1 pressed. Add compare values here if you need more than 2 buttons
				if (BwButton & (BW_EVBT2|BW_EVOPN)) {
					BwButton = BW_EVENT + BW_EVBT1;
					btn_cnt = 1;
				}
			}
			if (btn_cnt) {
				if(++btn_cnt > BW_LONG_PRESS) {
					// New event - long press
					BwButton |= BW_EVENT + BW_EVLNG;
					btn_cnt = 0;
				}
			}
			// Cleanup
			if (!(BwButton&(BW_EVBT1|BW_EVBT2|BW_EVOPN))) BwButton = BW_EVOPN;
		}
Если здесь не проскакивать насквозь стейт ADC_WAIT, то можно и седьмой бит в цикл мигания светоидода добавить. Но мне шести бит хватает. А для шестнадцатибитного BwState можно из светодиодов вообще сумасшедшую дискотеку устроить.
		if (color_mix++ > MAX_COLOR_MIX) {
			// Get next state (LED sequence)
			BwState = (BwState >> 7) | (BwState << 1); // rotate left, replace by _asm to compact code
			// Indicate
			DDRC |= _BV(ADCPORT);
			PORTC &= ~(_BV(GNDPORT)|_BV(ADCPORT));
			if(bwsignalsR[BwIndicate]&BwState)
				PORTC |= _BV(BWPORT2);
			if((bwsignalsG[BwIndicate]&BwState) && (!(bwsignalsR[BwIndicate]&BwState)) )
				PORTC |= _BV(BWPORT1);
			color_mix = 0;
		}
		if((bwsignalsR[BwIndicate]&BwState) && (bwsignalsG[BwIndicate]&BwState)){
			PORTC ^= (_BV(BWPORT1) | _BV(BWPORT2));
		}
	}
}

Скачать .c

Вот и все. Вставляйте этот код в свои исходники и экономьте провода и ноги. Из минусов — разве что несколько «тупая» реакция на кнопку, но к этому быстро привыкаешь.
Еще можно добавить третий массив после bwsignalsR/bwsignalsG, например bwsignalsB, и с его помощью делать dimming для плавного включения-выключения. Оставлю это вам как домашнее задание :)

Ах да, чуть не забыл. Пример работы с библиотекой:
#include "biwire.h"

int main(void)
{
	biwire_init();
	sei();

	while (1) {
		if ( BwButton & BW_EVENT ) {
			BwButton &= ~BW_EVENT; // Clear event flag
			if (BwButton & BW_EVLNG) {
				BwIndicate = OrangeRG;
				BwLedDelay = 5;
			} else if (BwButton & BW_EVOPN) {
				//BwIndicate = LedOff;
			} else if (BwButton & BW_EVBT1) {
				BwIndicate = OneRed;
				BwLedDelay = 5;
			} else if (BwButton & BW_EVBT2) {
				BwIndicate = OneGreen;
				BwLedDelay = 5;
			}
		}
	}
}


PS: Странно, что-то мне такое смутно помнится, что в старом движке можно было напрямую с PasteBin вставлять исходники? Зачем такую хорошую возможность убрали? Или я что-то путаю?
Теги:
Хабы:
Всего голосов 48: ↑42 и ↓6+36
Комментарии24

Публикации

Истории

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань