Выбор железа
После недолгого анализа, выбор пал на 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 корпуса, поэтому был некий страх, что не получится… Оно оказалось не сложно, не просто… в общем пришлось приложить некие усилия, но в итоге получилось! Показалось целесообразным прогревать не по одному выводу, а сразу группами — так и быстрее и проще.
Чем программировать?
ATtiny24A по умолчанию тактируется от внутреннего генератора и работает на частоте 1 МГц. Ну и пусть работает, меня это вполне устраивает. А вот чтобы USBasp стал с ним работать на такой частоте, ему пришлось припаять дополнительную перемычку (проводок на фото):
Место на плате было, а вот джампер китайцы припаять не удосужились… пришлось сделать за них.
В плане среды разработки выбор пал на 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;
}
}
}
#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)!
Немного поясню что здесь зачем (слева направо):
- Моток провода — это чувствительная катушка металлоискателя;
- Кнопка слева на макетке — вызов функции определения резонанса;
- Диод + резистор + конденсатор — это амплитудный детектор;
- Зелёная платка — адаптер с ATtiny24A на нём;
- Светодиод с резистором и большая чёрная коробка (это древний микроамперметр) — индикация ШИМ;
- Arduino Nano подключённая двумя проводками — приёмник для отладочной информации.
Схема:

- L1, C1 — колебательный контур;
- D1, C2, R2 — амплитудный детектор.
Чувствительность получилась слишком низкой, чтобы можно было использовать на практике. Гирю в 0.5 кг чувствует сантиметров с 7, а монету так вообще только если внутрь катушки кинуть. Но, в целом, устройство работает:
На записи видно как при помещении в катушку металлического предмета падают показания АЦП(на экране) и МК повышает ток через индикатор.
Что дальше?
Задача «поиграться с ATtiny» выполнена. Всё работает, всё хорошо. Граблей на пути оказалось даже меньше, чем ожидал. Но из-за указанного в начале факта (что даже более мощный STM8S103F3P6 стоит дешевле) причин делать что-то на AVR вижу только две: простота и хорошая документация. Ну, может быть, ещё в два раза больший максимально допустимый ток выхода в каких-то случаях может стать аргументом.
Комментарии (18)

eta4ever
07.09.2015 12:16+2Кстати, переход на SOIC, и вообще на SMD, сильно экономит время и деньги. Не перестаю радоваться.

mazayats
07.09.2015 13:30+1Много о ATtiny и практически ничего о самом металлоискателе. А я так надеялся… Идею о металлоискателе на основе ардуино или чего-то ардуино-подобного, смртфона/планшета я уже давно закидывал на соответствующие форумы. Но уважаемые мастера отвечали, что ничего путного (хотя бы на уровне того, что паяют сами или заводское сейчас) не получится.

Alexeyslav
07.09.2015 13:37+3Потому что качество металлоискателя зависит только от конструкции поисковой катушки, а электроника которая с ней работает — дело вторичное.

mazayats
08.09.2015 13:35Все не так просто. Хорошая катушка важна, конечно. Хорошая — в плане чтобы правильно подобрали сопротивление, ровно поставили контуры, хорошо свели. Но катушка — это только антена, коотрая посылает/принимает сигнал. Все остальное делает электроника. Отстройка от грунта? Дискрим? А скорость обработки сигнала, когда некоторые приборы выдают сигнал через время, когда катушка уже в полуметре от цели? Никакая катушка не поставит асю на уровень 3030, иначе Нел давно купили бы всех, от Майнлаба до болгар с их Голден Маском, и продавали бы альтернативные блоки для своих катушек, а не наоборот. Я уже около 10 лет работаю с металлоискателями. Бывало мой самодельный прибор с самодельной катушкой видел хорошие вещи после аси с нелом или терки. Бывало асьководы просили меня посмотреть что пищит в ямке и стоит ли его копать. Неловские заводские катушки хуже тех, что делают отечественные мастера на кухне? Так что не соглашусь. Хорошая электроника с плохой катушкой нормально работать не будет, но плохая электроника с хорошей катушкой тоже превратит коп в «веселое» занятие.

Alexeyslav
08.09.2015 13:46Это всё сервисные функции, удобство пользования. Если катушка не будет работать нормально никакая электроника не спасёт, она как раз и определяет потолок возможностей металлоискателя а дальше всё зависит от электроники в том числе и сервисные функции вроде отстройки от грунта.
Я бы еще попробовал реализовать магнитоискатель, визуализирующий распределение магнитного поля. Используя подсветку в виде неодимового магнита можно выявлять даже и немагнитные материалы, ведь так или иначе магнитная проницаемость у них будет отличаться от грунта.

xedas
07.09.2015 15:30Путного ничего и не получилось: чувствительность слишком низкая. Гирю в 0.5 кг чувствует сантиметров с 7, а монету так вообще только если внутрь катушки кинуть. Этот опыт более ценен, тем, что научился программировать ATtiny — этому и больше внимания. Экспериментировал с разными катушками, в итоге этот моток провода показал наилучшие результаты. Ниже выложил схему.

Alexeyslav
07.09.2015 16:39Многое зависит от диаметра катушки, а больше диаметр больше производственных помех ловит.
Увеличивать диаметр выше не имеет смысла — чувствительность вырастет а толку не будет — почва, перекрытия и т.д. достаточно сильно экранируют даже металлические предметы поскольку их обычно на порядки больше искомого металла хоть они и имеют меньшую магнитную проницаемость действие на катушку существенно.
Собственно, по сути у вас получился металлоискатель не лучше конструкции на биениях или прямым измерением частоты.

ks0
07.09.2015 14:44А где сама схема-то, мне непонятно как выглядит амплитудный детектор, какие там номиналы. Как выглядит конденсатор параллельный катушки, он есть вообще? И какая примерно индуктивность у такой катушки получилась. Вообще да, о металлоискателе очень мало.

xedas
07.09.2015 15:22Схема такая:

Померить индуктивность не могу, разве, что рассчитать зная резонансную частоту — около 25 кГц.
Alexeyslav
07.09.2015 16:43+2Детектор бы на операционнике и желательно после повторителя с большим входным сопротивлением — так можно будет чётче поймать точку резонанса.
Кстати, в хороших металоискателях с хорошей катушкой резонансная частота получается в районе 1...7кГц. 25 — это многовато.
xedas
08.09.2015 15:17Изменил схему вот на такую:

+ на МК включил усиление x20 перед АЦП.
В результате:
— Амплитуда колебаний упала до ~125 мВ;
— Резонанс стал гораздо более выраженный, пик острей из-за увеличения R1 в 8 раз;
— Чувствительность возрасла: теперь 50и копеечную монету уверенно чует с трёх сантиметров от края катушки.
Пробовал ставить R1 = 220 кОм — амплитуда ещё упала, но чувствительность уже не увеличивается.
Мысли как ещё можно улучшить:
1. Вместо R1 поставить источник тока. Возможно, это увеличит добротность контура и за счёт большей амплитуды колебаний уменьшит чувствительность к помехам.
2. Чтобы точней ловить резонанс, повысить частоту МК с 1 до 8 МГц.
Не уверен нужен ли тут R6. Вроде, по даташиту выход ОУ можно замыкать на землю, но на всякий случай поставил.
eta4ever
Про пайку — недогрев, мало флюса.
AVRки мешками стоят копейки. Может, сейчас цены поменялись, но помнится, покупал Mega8 в QFP и STM32F030 в TSSOP по полбакса где-то (при заказе десятка).
eta4ever
Не успел дописать. Мысль-то была в том, что за одни и те же деньги, только STM32 куда веселее. Но на AVR как-то быстрее получается что-нибудь сляпать. За STM8 ничего не скажу.
ploop
eta4ever
Да я так смотрю, не принципиально дешевле 32F030, но последний уже ARM со всеми плюшками, хоть в mbed его, хоть куда.
ploop
Ну китайцы предпочитают совать всюду именно STM8, если партия не дотягивает до аппаратно захардкоженных чипов, а в последнее время вообще какие-то свои контроллеры. Возможно от партии зависит…
Alexeyslav
Склады опустошают. Наштамповали контроллеров и их надо куда-то девать…
xedas
Да, действительно, штырьки не пропаял. На фотографии на весь экран дефекты видно лучше, чем в реальности на 2х сантиметровой плате. Спасибо, что обратили внимание — учту в будущем!