Делаем металлоискатель на ATtiny24A

от автора

Долгое время игрался с Arduino-подобными платами, но всё время хотелось «меньше, дешевле и ближе к железу!», и вот — первый опыт программирования чистого ATtiny. В статье не будет какой-то особо эффективной схемы металлоискателя. Это всего лишь демонстрация того, на что способен микроконтроллер за 47 центов + путь чайника в этом, как выяснилось, совсем не сложном деле перехода от Arduino на уровень ниже.

Выбор железа

После недолгого анализа, выбор пал на ATtiny24A-SSU (14-pin SOIC корпус). Почему? Причина проста: цена + ядро AVR. Да, я знаю, что даже более мощный STM8S103F3P6 стоит дешевле (39,5 центов за штуку против 47 за ATtiny), но имея какой-то опыт работы с AVR в Arduino хотелось для первых экспериментов именно AVR.

Из доступных AVR выбираем ATtiny как самые дешёвые, а дальше хочется DIP корпус как более простой для пайки. Но микросхемы в DIP корпусе оказались гораздо дороже (54 цента за 8-ногий ATtiny13A, а 14-ногий ATtiny23A в DIP корпусе так вообще 95 центов). Идея использовать ATtiny13A мне не нравится из-за его восьминогости. 6 ног будут заняты программатором и остаются всего 2 свободные, что мало.

Было принято решение купить ATtiny24A-SSU по 47 центов и ещё переходники по 30 центов. Итого получаем 77 центов на устройство против 95 за DIP корпус и, как бонус, в простых устройствах использовать переходник в качестве платы с подпаиванием проводков прямо к нему, что было бы невозможно с DIP корпусом.

Программатор выбран по тому же принципу (самый дешёвый): USBasp за 1,86$.

Приехало!

Сразу скажу, что никогда не паял раньше SOIC корпуса, поэтому был некий страх, что не получится… Оно оказалось не сложно, не просто… в общем пришлось приложить некие усилия, но в итоге получилось! Показалось целесообразным прогревать не по одному выводу, а сразу группами — так и быстрее и проще.

image

Чем программировать?

ATtiny24A по умолчанию тактируется от внутреннего генератора и работает на частоте 1 МГц. Ну и пусть работает, меня это вполне устраивает. А вот чтобы USBasp стал с ним работать на такой частоте, ему пришлось припаять дополнительную перемычку (проводок на фото):

image

Место на плате было, а вот джампер китайцы припаять не удосужились… пришлось сделать за них.

В плане среды разработки выбор пал на Atmel Studio, однако она не поддерживает наш USBasp… но это же не беда! Ещё при выборе программатора планировалось перепрошить его в AVR-Doper, который совместим с STK500, а значит поддерживается нашей Atmel Studio. В общем, прошивал я его много раз разными прошивками, но Atmel Studio никак не хотела его видеть… печаль… в итоге отчаялся, прошил обратно в USBasp и сделал по инструкии. После чего удалось прошить свою ATtiny, помигать светодиодом и обрадоваться тому как мало flash памяти по сравнению с Arduino это заняло.

Металлоискатель

Ещё когда баловался с Arduino, делал металлоискатель работающий на принципе срыва резонанса. Чувствительность ужасная, однако принцип работы очень прост и легко реализуется на любом МК. На паралельный колебательный контур через резистор подаётся прямоугольный сигнал на резонансной частоте этого контура. Когда в магнитное поле катушки попадает металлический предмет, добротность контура падает, амплитуда сигнала, измеряемая АЦП, падает, устройство радует нас визуально и акустически.

У металлоискателя 2 режима:
1. Поиск резонанса контура. При этом он посылает на контур прямоугольные сигналы разной частоты и запоминает частоту, при которой амплитуда колебаний будет наибольшей (эту наибольшую амплитуду тоже запоминаем).
2. Рабочий режим. На контур посылаем сигнал с резонансной частотой и сравниваем амплитуду с тем максимумом, который был в первом режиме.

Сложно? — Нет!
Много памяти должно занимать? — Нет!
А много памяти у нас есть (2 KB flash + 128 байт оперативки)? — Тоже нет!
Влезет? Попробуем — узнаем!

В итоге, влезло.

Основной код прошивки

 #include <avr/io.h> #include <avr/interrupt.h> #include "mySerial.cpp"  MySerial ms(&PORTB, &PINB, &DDRB, 0, &PORTB, &PINB, &DDRB, 1);  volatile uint16_t maxAdc = 0; // максимальное показание АЦП (в резонансе на максимальной добротности) volatile uint8_t dispMode = 0; // 0 - поиск резонанса, 1 - рабочий режим volatile uint8_t flags0 = 0; // [0] - need setRes volatile uint16_t adcSource = 0; //volatile bool needADC = false; #define ADC_SOURCE_ARRAY_SIZE_POWER 5 #define ADC_SOURCE_ARRAY_SIZE (1 << ADC_SOURCE_ARRAY_SIZE_POWER) uint16_t adcSourceArray[ADC_SOURCE_ARRAY_SIZE]; uint8_t adcSourceArrayLastWrited = 0; void showVal(void);  ISR(ADC_vect){ 	//adcSourceArrayLastWrited++; 	if(++adcSourceArrayLastWrited >= ADC_SOURCE_ARRAY_SIZE) 		adcSourceArrayLastWrited = 0; 	adcSourceArray[adcSourceArrayLastWrited] = ADCL | (ADCH << 8); 	uint16_t adcSourceTmp = 0; 	for(uint8_t i = 0; i < ADC_SOURCE_ARRAY_SIZE; i++) 		adcSourceTmp += adcSourceArray[adcSourceArrayLastWrited]; 	adcSource = (adcSourceTmp >> ADC_SOURCE_ARRAY_SIZE_POWER); 	//adcSource = ADCL | (ADCH << 8); 	//needADC = false; }  volatile uint8_t pinaChanged = 0; volatile uint8_t tim0_ovf_counter = 0; //uint32_t ticks = 0; volatile uint16_t ticks10ms = 0; //volatile uint16_t ticks = 0; ISR(TIM0_OVF_vect) {		 	//ticks++; 	//if(255 == tim0_ovf_counter++){ // ticks every 65.5 ms 	if(39 == (tim0_ovf_counter++)){ // ticks every 10 ms 		tim0_ovf_counter = 0; 		ticks10ms++; 		if(pinaChanged > 0) 			pinaChanged--; 	} }  uint16_t dist16(uint16_t lo, uint16_t hi){ 	return (lo <= hi) ? (hi - lo) : (0xFFFF - lo + hi); } /*void delayTicks(uint16_t val){ 	uint16_t tim0_ovf_counter0 = tim0_ovf_counter; 	while(dist16(tim0_ovf_counter0, tim0_ovf_counter) < val) 		showVal(); }*/ void delay10ms(uint16_t val){ 	uint16_t ticks10ms0 = ticks10ms; 	while(dist16(ticks10ms0, ticks10ms) < val) 		showVal(); }  void showVal(void){ 	ms.sendByte(adcSource >> 2); 	switch(dispMode){ 		case 0: 			OCR0A = adcSource >> 2; 		break; 		case 1: 			uint16_t maxAdcPlus = maxAdc + 2; 			uint16_t dispVal = (maxAdcPlus > adcSource) ? ((maxAdcPlus - adcSource)) : 0; 			dispVal <<= 4; 			if(dispVal > 255) 				dispVal = 255; 			OCR0A = dispVal; 		break; 	} }  void setRes(void) { 	dispMode = 0; 	uint16_t maxOCR = 0; 	maxAdc = 0; 	for(uint16_t curOCR = 35; curOCR < 50; curOCR++){ 		OCR1A = curOCR; 		OCR1B = (curOCR >> 1); 		//uint32_t ticks0 = ticks; 		//uint16_t ticks0 = ticks;		 		//while(dist16(ticks0, ticks) < 20) 		//	showVal(); 		delay10ms(30); 		if(adcSource > maxAdc){ 			maxAdc = adcSource; 			maxOCR = curOCR; 		} 	} 	OCR1A = maxOCR; 	OCR1B = (maxOCR >> 1); 	dispMode = 1; }  ISR(PCINT0_vect) {	 	if(pinaChanged > 0) 		return; 	pinaChanged = 5; 	if(0 == (PINA & (1 << 7))) 		flags0 |= 1; }   int main(void) { 	// init PWM: 	DDRB |= 4;  // OC0A as output 	//TIMSK0 |= 7; // разрешаем TIM0_OVF_vect, TIM0_COMPA_vect, TIM0_COMPB_vect 	TIMSK0 |= 1; // разрешаем TIM0_OVF_vect 	TCCR0B |= 1; // no prescaling. OVF каждые 256 мкс (3.91 кГц) 	//TCCR0B |= 2; // clk/8 	//TCCR0B |= 3; // clk/64 	//TCCR0B |= 5; // clk/1024. OVF каждые 262 мс (3.815 Гц) 	TCCR0A |= (3 | (1 << 7)); //WGM0[2:0] = 3 - fawt PWM mode. bit7 - дёргать ногой 	//OCR0A = 150; 	//OCR0B = 100; 	// :init PWM 	 	// init ADC: 	//ADMUX |= (1 << 7); // internal 1.1V reference. Comment this to use VCC as reference 	//ADMUX |= (1 << 3) | 1; // MUX[5:0] = 001001. Res = ADC0 - ADC1. Gain = 20 	ADMUX |= (1 << 3); // MUX[5:0] = 001000. Res = ADC0 - ADC1. Gain = 1 	ADCSRA |= ((1 << 7) // enable ADC 		| (1 << 5) // ADC Auto Trigger Enable. Постоянно работает 		| (1 << 6) // запускаем 1е преобразование 		| (1 << 3)   // ADC interrupt enable 		| (1 << 2)); // prescaller = 16 (надо 50-200 kHz) 	// :init ADC 	 	// init 16-bit timer: // pin7 = MOSI = PA6 = OC1A 	//DDRA |= (1 << 6); // OC1A as output 	DDRA |= (1 << 5); // OC1B as output 	//TCCR1A |= (1 << 6); // Toggle OC1A/OC1B on Compare Match 	TCCR1A |= (1 << 5) // Clear OC1B on Compare Match, set OC1B at BOTTOM (non-inverting mode) 		| (3); // set WGM10 and WGM11 // WGM1[3:0] = 1111 - Fast PWM, TOP = OCR1A.	 //	TCCR1A |= (1 << 6) | (1 << 7) // Set OC1A on Compare Match (Set output to high level). //			| (1 << 5); // Clear OC1B on Compare Match	(Set output to low level) 	TCCR1B |= 1 // no prescalling 			| (1 << 3) | (1 << 4); // set WGM12 and WGM13 	//TIMSK1 |= (1 << 2) | (1 << 1) | 1; // enable all interrupts 	OCR1B = 21; 	OCR1A = 42; 	//for(;;){;}; 	// :init 16-bit timer 	 	// init button: 	PORTA |= (1 << 7); // включаем подтягивающий резистор на 6-й ноге. PA7 = PCINT7 	GIMSK |= (1 << 4); // Pin Change Interrupt Enable 0 	PCMSK0 |= (1 << 7); // включаем прерывание PCINT7 	// :init button 	 	sei(); 	flags0 = 1; // это экономит 22 байта по сравнению с присвоением при объявлении! 	while(1){ 		showVal(); 		//ms.sendByte(0x99); 		if(0 != (1 & flags0)){ 			setRes(); 			flags0 &= ~1; 		} 	} } 

И mySerial.cpp

 #include <avr/io.h> #include <avr/interrupt.h>  class MySerial{ 	public: 	volatile uint8_t *dataPort; 	volatile uint8_t *dataPin; 	volatile uint8_t *dataDDR; 	volatile uint8_t *clockPort; 	volatile uint8_t *clockPin; 	volatile uint8_t *clockDDR; 	uint8_t dataPinMask, clockPinMask; 	uint8_t rBit, 		lastState, // (dataPin << 1) | clockPin 		inData; 	// MySerial ms(&PORTD, &PIND, &DDRD, 2, &PORTD, &PIND, &DDRD, 3); 	MySerial( 		volatile uint8_t *_dataPort, 		volatile uint8_t *_dataPin, 		volatile uint8_t *_dataDDR, 		uint8_t _dataPinN, 		volatile uint8_t *_clockPort, 		volatile uint8_t *_clockPin, 		volatile uint8_t *_clockDDR, 		uint8_t _clockPinN 	){ 		rBit = 255; 		lastState = 3; 		inData = 0; 		dataPort = _dataPort; 		dataPin = _dataPin; 		dataDDR = _dataDDR; 		dataPinMask = (1 << _dataPinN); 		clockPort = _clockPort; 		clockPin = _clockPin; 		clockDDR = _clockDDR; 		clockPinMask = (1 << _clockPinN); 	} 	void dataZero() { 		*dataPort &= ~dataPinMask; //digitalWrite(pinData, 0); 		*dataDDR |= dataPinMask;   //pinMode(pinData, OUTPUT); 	} 	void dataRelease() { 		*dataDDR &= ~dataPinMask; //pinMode(pinData, INPUT); 		*dataPort |= dataPinMask; //digitalWrite(pinData, 1); 	} 	void clockZero() { 		*clockPort &= ~clockPinMask; //digitalWrite(pinClock, 0); 		*clockDDR |= clockPinMask;  //pinMode(pinClock, OUTPUT); 	} 	void clockRelease() { 		*clockDDR &= ~clockPinMask; //pinMode(pinClock, INPUT); 		*clockPort |= clockPinMask; //digitalWrite(pinClock, 1); 	} 	void pause() { 		//delay(v * 1); 		//unsigned long time = micros(); 		//while(v-- > 0) 		for(uint16_t i = 0; i < 250; i++) 			__asm__ __volatile__( 				"nop" 			); 		//time = micros() - time; 		//LOG("Paused "); LOG(time); LOGLN("us"); 	} 	void sendByte(uint8_t data){ 		//LOG("Sending byte: "); LOGLN(data); 		// отрицательный фронт data при clock = 1: 		dataRelease(); 		clockRelease(); 		pause(); 		dataZero(); 		pause(); 		 		//LOGLN("Going to loop..."); 		for(uint8_t i = 0; i < 8; i++){ 			clockZero(); 			pause(); 			if( 0 == (data & (1 << 7)) ) 				dataZero(); 			else 				dataRelease(); 			//LOG("Sending bit "); LOGLN((data & (1 << 7))); 			pause(); 			clockRelease(); 			pause();  			data = data << 1; 		} 		 		// положительный фронт data при clock = 1: 		dataZero(); 		pause(); 		dataRelease(); 		pause(); 	}	 	void tick(){ 		//uint8_t curState = (digitalRead(pinData) << 1) | digitalRead(pinClock); 		dataRelease(); 		clockRelease(); 		uint8_t curState = 0; 		if(0 != (*dataPin & dataPinMask)) 			curState |= 2; 		if(0 != (*clockPin & clockPinMask)) 			curState |= 1;  		//LOGLN(curState); 		if((3 == lastState) && (1 == curState)) // началась передача 		rBit = 7; 		if(255 != rBit) 		if( (0 == (lastState & 1)) && (1 == (curState & 1)) ) { // пришёл положительный фронт clock 			//LOG("Getted bit "); LOGLN((curState >> 1)); 			if( 0 == (curState >> 1) ) 				inData &= ~(1 << rBit); 			else 				inData |= (1 << rBit); 			rBit--; 		} 		 		if( (1 == lastState) && (3 == curState) ){ // закончилась передача 			//LOG("Recieved byte: "); LOGLN(inData); 			rBit = 255; 			//delay(5000); 		} 		lastState = curState; 	} }; 

И мало того, что влезло, так оно и занимает всего 1044 байта во flash из доступных 2048! И это при том, что помимо основной функции, он ещё отправляет отладочную информацию (MySerial)!

image

Немного поясню что здесь зачем (слева направо):

  • Моток провода — это чувствительная катушка металлоискателя;
  • Кнопка слева на макетке — вызов функции определения резонанса;
  • Диод + резистор + конденсатор — это амплитудный детектор;
  • Зелёная платка — адаптер с ATtiny24A на нём;
  • Светодиод с резистором и большая чёрная коробка (это древний микроамперметр) — индикация ШИМ;
  • Arduino Nano подключённая двумя проводками — приёмник для отладочной информации.

На записи видно как при помещении в катушку металлического предмета падают показания АЦП(на экране) и МК повышает ток через индикатор.

Что дальше?

Задача «поиграться с ATtiny» выполнена. Всё работает, всё хорошо. Граблей на пути оказалось даже меньше, чем ожидал. Но из-за указанного в начале факта (что даже более мощный STM8S103F3P6 стоит дешевле) причин делать что-то на AVR вижу только две: простота и хорошая документация. Ну, может быть, ещё в два раза больший максимально допустимый ток выхода в каких-то случаях может стать аргументом.

ссылка на оригинал статьи http://geektimes.ru/post/262004/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *