Pull to refresh

Делаем модульный многоканальный АЦП

Reading time4 min
Views46K
В различных проектах часто бывает необходимо следить за множеством параметров, которые представлены аналоговыми величинами. Конечно, часто хватает микроконтроллера, но иногда алгоритм обработки слишком сложен для него и требуется использование полноценного компьютера. К тому же на нём гораздо проще организовывать сохранение логов и красивую визуализацию данных. В таком случае либо берётся готовое промышленное решение (которое, разумеется, стоит дорого, но часто является избыточным), либо делается что-то самодельное. В самом банальном случае это может быть плата Arduino с бесконечным циклом из analogRead и serial.write. Если входных данных много (больше, чем аналоговых входов), то потребуется несколько плат, придумывать как их правильно опрашивать с компьютера и т. д. Во многих случаях подойдёт разработанное мною решение (возможно, я не первый придумал именно такую реализацию, не особо интересовался этим вопросом), которое позволит сэкономить время на отладку и сделать относительную простую и понятную архитектуру системы.



Чтобы понять, подойдёт ли это решение вам, предлагаю ознакомится с его характеристиками:

Максимальное число каналов: 44;
Частота дискретизации: 1000 Герц;
Разрешение: 8 бит.

Характеристики достаточно посредственные, однако для многих задач могут подойти. Это ведь не осциллограф, а система опроса датчиков. К тому же на её примере можно познакомится с использованием USART не совсем по назначению.

Система состоит из отдельных модулей АЦП на базе микроконтроллера ATMEGA8 (можно применить другой МК семейства AVR с АЦП и аппаратным модулем USART, если немного изменить прошивку). Модулей может быть один или несколько, каждый предоставляет 6 или 8 АЦП в зависимости от корпуса микроконтроллера (выводная версия имеет 6 АЦП, а для поверхностного монтажа 8), только суммарное количество каналов не должно превышать 44. Главная особенность в том, что вне зависимости от количества модулей требуется лишь один USART со стороны компьютера (это может быть USB-переходник или аппаратный COM-порт). Это достигается засчёт того, что USART'ы всех микроконтроллеров соединяются последовательно (RX одного к TX другого), а RX и TX пины крайних в цепочке уже подсоединяются к компьютеру.

Тут надо заметить то, что разрядность моего АЦП не совсем 8 бит — возможно лишь 255 градаций вместо 256. Значение 0xFF зарезервировано для особой цели. Если микроконтроллер получает его, то начинает выдавать каждый раз значение с очередного канала своего АЦП, а когда они кончаются ретранслирует 0xFF дальше по цепочке. Если же на вход USART приходит значение отличное от 0xFF, то микросхема просто пересылает байт далее. Таким образом передав одно произвольное значение и 44 0xFF можно получить значения со всех каналов всех АЦП (если АЦП меньше, то лишние каналы будут равны 0xFF). Произвольное значение нужно для того, чтобы все модули сбросили указатель на текущий канал АЦП, который надо передавать при получении 0xFF. В реальности удобнее передавать 45 0xFF, чтобы надёжно определять окончание приёма (если получили 0xFF, значит каналы закончились).

Принципиальная схема содержит не очень много деталей:


Программа для AVR выглядит предельно просто и занимает чуть меньше 300 байт памяти:

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>
#include <util/delay.h>
// Firmware options
#define USART_BAUDRATE 460800
#define LED_PIN 1
#define ADC_COUNT 6
#define STARTUP_DELAY 1000
// Calculated UBRR value
#define UBRR (F_CPU / (16 * (uint32_t)USART_BAUDRATE) - 1)
// Global variables
uint8_t adc[ADC_COUNT]; // Buffer
uint8_t cur_in_adc; // Input byte index
uint8_t cur_out_adc; // Output byte index
// USART interrupt handler
ISR(USART_RXC_vect) {
	// Read data from USART
	uint8_t buffer = UDR;
	if (buffer == 0xFF) {
		if (cur_out_adc < ADC_COUNT) {
			// Return data byte from buffer
			UDR = adc[cur_out_adc];
			cur_out_adc++;
			// Activate led
			PORTB |= _BV(LED_PIN);
		} else {
			// Chain 0xFF
			UDR = 0xFF;
			// Deactivate led
			PORTB &= ~_BV(LED_PIN);
		}
	} else {
		// Chain data byte
		UDR = buffer;
		// Reset byte counter
		cur_out_adc = 0;
		// Deactivate led
		PORTB &= ~_BV(LED_PIN);
	}
}
// Main function
void main() {
	// Setup watchdog timer
	wdt_enable(WDTO_15MS);
	// Setup pin for led
	DDRB |= _BV(LED_PIN);
	// Blink led
	PORTB |= _BV(LED_PIN);
	for (uint8_t i = 0; i < STARTUP_DELAY / 5; i++) {
		_delay_ms(5);
		wdt_reset();
	}
	PORTB &= ~_BV(LED_PIN);
	// Setup ADC
	ADMUX = _BV(REFS1) | _BV(REFS0) | _BV(ADLAR);
	ADCSRA = _BV(ADEN) | _BV(ADPS2) | _BV(ADPS1) | _BV(ADPS0);
	// Setup USART
	UBRRL = UBRR & 0xFF;
	UBRRH = UBRR >> 8;
	UCSRA = 0;
	UCSRB = _BV(RXCIE) | _BV(RXEN) | _BV(TXEN);
	UCSRC = _BV(URSEL) | _BV(UCSZ1) | _BV(UCSZ0);
	// Enable interrupts
	sei();
	// Main loop
	while (1) {
		// Reset watchdog timer
		wdt_reset();
		// Select ADC channel
		ADMUX = _BV(REFS1) | _BV(REFS0) | _BV(ADLAR) | cur_in_adc;
		// Start conversion and wait until it performed
		ADCSRA |= _BV(ADIF) | _BV(ADSC);
		while (ADCSRA & _BV(ADSC));
		// Put value from ADC to buffer
		uint8_t value = ADCH;
		adc[cur_in_adc] = (value != 0xFF) ? value : 0xFE;
		// Switch to next channel
		cur_in_adc++;
		if (cur_in_adc >= ADC_COUNT) {
			cur_in_adc = 0;
		}
	}
}


В репозитории на GitHub вы можете найти файлы схемы и печатной платы KiCad, а также пример программы для компьютера, которая читает данные с этой системы и выдаёт их в формате CSV.

Удобно, что последовательный порт используется практически на пределе своих возможностей, поэтому не требуется заботится о синхронизации данных (я просто отправляю каждую секунду сразу 1000 команд на чтение АЦП) — если мы передаём 46 килобайт данных каждую секунду со скоростью 460800 бит в секунду, то можно быть полностью уверенным, что блоки из 46 байт данных (один замер) будут приходить каждую миллисекунду (хотя буферизация ядром ОС и USB-переходником, конечно, внесёт задержку, но замеры всегда будут производится с нужной частотой).

Печатная плата была спроектирована в KiCad:



Все платы соединяются в цепочку, у последней платы RX и TX соединяются джампером.
Качество работы АЦП можно оценить по этому изображению пилы на 10 Гц:



Для сравнения изображение с осциллографа DS203 (он же и выступает генератором):



К сожалению, у меня нет более качественного источника сигнала, но для низкочастотных сигналов моя система должна подойти.

Надо отметить, что не каждый преобразователь USART-USB обеспечивает скорость 460800 бит/сек при полной загрузке канала. Преобразователь на базе CP2102 заставил меня долго искать ошибку в собственном коде, пока я не попробовал FT232. Также наблюдается потеря порядка 0.17% данных (в программе для компьютера приняты меры, чтобы не терялась синхронизация данных). Скорее всего это вызвано плохой линией USART, либо недоработкой в программе. В общем, для 90% применений не должно быть критично, но ставить на АЭС скорее всего не стоит.

Ну и напоследок скажу, что себестоимость одного модуля получается около 50-60 рублей, если заказывать все детали из Китая, так что решение должно быть достаточно привлекательным.
Tags:
Hubs:
+36
Comments14

Articles

Change theme settings