В различных проектах часто бывает необходимо следить за множеством параметров, которые представлены аналоговыми величинами. Конечно, часто хватает микроконтроллера, но иногда алгоритм обработки слишком сложен для него и требуется использование полноценного компьютера. К тому же на нём гораздо проще организовывать сохранение логов и красивую визуализацию данных. В таком случае либо берётся готовое промышленное решение (которое, разумеется, стоит дорого, но часто является избыточным), либо делается что-то самодельное. В самом банальном случае это может быть плата 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;
}
}
}
А вот простой пример программы для компьютера. Она принимает в качестве параметра имя последовательного порта и начинает выдавать на stdout данные в формате CSV, а на stderr статистику (байт передано, получено и сколько замеров произведено за секунду). Можно просто перенаправить её вывод в файл, а потом открыть его в Excel, Calc или более подходящей программе, а можно легко использовать её в качестве бэкэнда в своём приложении, перехватив её вывод. Приложение изначально написано под Linux, но в теории может быть собрано с использованием Cygwin для ОС семейства Windows.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <time.h>
#include <signal.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/select.h>
// Settings
#define DEFAULT_BAUDRATE 460800
#define MAX_CHANNEL_COUNT 44
#define READ_BUFFER_SIZE 256
// Global variables
volatile sig_atomic_t write_buffer_offset = INT_MAX;
volatile sig_atomic_t remaining_writes = 0;
volatile sig_atomic_t write_counter = 0;
volatile sig_atomic_t read_counter = 0;
volatile sig_atomic_t sample_counter = 0;
int adc_data[MAX_CHANNEL_COUNT];
int adc_count = 0;
int cur_adc = -1;
uint8_t command[(MAX_CHANNEL_COUNT + 2) * 1000];
// Print usage information
void print_usage(char *program_name) {
fprintf(stderr, "Usage: %s devicen", program_name);
fprintf(stderr, "tdevice - path to serial device (e.g. /dev/ttyS0 or /dev/ttyUSB0)n");
fprintf(stderr, "n");
}
// Open serial port
int open_serial_device(char *path) {
// Open device
int fd = open(path, O_RDWR | O_NONBLOCK);
if (fd == -1) return -1;
// Get current options
struct termios options;
tcgetattr(fd, &options);
// Set baudrate
cfsetspeed(&options, DEFAULT_BAUDRATE);
// Set mode (8N1)
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
// Disable hardware flow control (if available)
#ifdef CNEW_RTSCTS
options.c_cflag &= ~CNEW_RTSCTS;
#elifdef CRTSCTS
options.c_cflag &= ~CRTSCTS;
#endif
// Set new options
tcsetattr(fd, TCSANOW, &options);
// Return handle
return fd;
}
// Alarm handler
void alarm_handler(int sig) {
// Check for timeout
static int first_run = 1;
if (first_run) {
first_run = 0;
} else if (!sample_counter) {
fprintf(stderr, "Timeoutn");
exit(-2);
}
// Send next command
if (write_buffer_offset >= sizeof(command)) {
write_buffer_offset = 0;
} else {
remaining_writes++;
}
// Display debug info
fprintf(stderr, "Writing %i bps, reading %i bps, %i samples per secondn", write_counter, read_counter, sample_counter);
// Reset performance counter
read_counter = 0;
write_counter = 0;
sample_counter = 0;
}
// Print ADC data
void print_adc_data() {
int i;
for (i = 0; i < adc_count; i++) {
if (i) {
printf(",%i", adc_data[i]);
} else {
printf("%i", adc_data[i]);
}
}
printf("n");
}
// Main function
int main(int argc, char **argv) {
if (argc < 2) {
print_usage(argv[0]);
} else {
// Open serial port
char *device = argv[1];
int device_fd = open_serial_device(device);
if (device_fd == -1) {
fprintf(stderr, "Failed to open %s: %sn", device, strerror(errno));
return -1;
}
// Setup alarm signal handler
{
struct sigaction sig;
sig.sa_handler = alarm_handler;
sigemptyset(&sig.sa_mask);
sig.sa_flags = SA_RESTART;
sigaction(SIGALRM, &sig, NULL);
}
// Setup timer
timer_t timer;
if (timer_create(CLOCK_MONOTONIC, NULL, &timer)) {
perror("timer_create() failedn");
close(device_fd);
return -1;
}
{
struct itimerspec timer_spec;
timer_spec.it_interval.tv_sec = 1;
timer_spec.it_interval.tv_nsec = 0;
timer_spec.it_value.tv_sec = 0;
timer_spec.it_value.tv_nsec = 1;
if (timer_settime(timer, 0, &timer_spec, NULL) < 0) {
perror("timer_settime() failed");
close(device_fd);
return -1;
}
}
// Generate USART command
{
int i;
memset(command, 0xFF, sizeof(command));
for (i = 0; i < sizeof(command); i += MAX_CHANNEL_COUNT + 2) {
command[i] = 0;
}
}
// Main loop
while (1) {
// Wait device ready for reading or writing
fd_set fds_r, fds_w;
FD_ZERO(&fds_r);
FD_ZERO(&fds_w);
FD_SET(device_fd, &fds_r);
FD_SET(device_fd, &fds_w);
int retval = select(FD_SETSIZE, &fds_r, &fds_w, NULL, NULL);
// Check for errors
if (retval < 0) {
if (errno == EINTR) continue;
perror("select() failed");
timer_delete(timer);
close(device_fd);
return -1;
}
// Read data
if (FD_ISSET(device_fd, &fds_r)) {
uint8_t buffer[READ_BUFFER_SIZE];
int bytes_count;
while ((bytes_count = read(device_fd, buffer, sizeof(buffer))) > 0) {
read_counter += bytes_count;
int i;
for (i = 0; i < bytes_count; i++) {
if (buffer[i] == 0xFF) {
if (adc_count) {
print_adc_data();
sample_counter++;
adc_count = 0;
}
cur_adc = -1;
} else {
if ((cur_adc > -1) && (cur_adc < MAX_CHANNEL_COUNT)) {
if (buffer[i] != 0xFF) {
adc_data[cur_adc] = buffer[i];
adc_count++;
}
}
cur_adc++;
}
}
}
}
// Write data
if (FD_ISSET(device_fd, &fds_w) && (write_buffer_offset < sizeof(command))) {
int bytes_count;
while ((bytes_count = write(device_fd, command + write_buffer_offset, sizeof(command) - write_buffer_offset)) > 0) {
write_counter += bytes_count;
write_buffer_offset += bytes_count;
}
if ((write_buffer_offset >= sizeof(command)) && remaining_writes) {
write_buffer_offset = 0;
remaining_writes--;
}
}
}
// Cleanup
timer_delete(timer);
close(device_fd);
}
return 0;
}
Удобно, что последовательный порт используется практически на пределе своих возможностей, поэтому не требуется заботится о синхронизации данных (я просто отправляю каждую секунду сразу 1000 команд на чтение АЦП) — если мы передаём 46 килобайт данных каждую секунду со скоростью 460800 бит в секунду, то можно быть полностью уверенным, что блоки из 46 байт данных (один замер) будут приходить каждую миллисекунду (хотя буферизация ядром ОС и USB-переходником, конечно, внесёт задержку, но замеры всегда будут производится с нужной частотой).
Печатная плата была спроектирована в KiCad:
Все платы соединяются в цепочку, у последней платы RX и TX соединяются джампером.
Качество работы АЦП можно оценить по этому изображению пилы на 10 Гц:
Для сравнения изображение с осциллографа DS203 (он же и выступает генератором):
К сожалению, у меня нет более качественного источника сигнала, но для низкочастотных сигналов моя система должна подойти.
Надо отметить, что не каждый преобразователь USART-USB обеспечивает скорость 460800 бит/сек при полной загрузке канала. Преобразователь на базе CP2102 заставил меня долго искать ошибку в собственном коде, пока я не попробовал FT232. Также наблюдается потеря порядка 0.17% данных (в программе для компьютера приняты меры, чтобы не терялась синхронизация данных). Скорее всего это вызвано плохой линией USART, либо недоработкой в программе. В общем, для 90% применений не должно быть критично, но ставить на АЭС скорее всего не стоит.
Ну и напоследок скажу, что себестоимость одного модуля получается около 50-60 рублей, если заказывать все детали из Китая, так что решение должно быть достаточно привлекательным.
Автор: KivApple