Обновить
13
0

Пользователь

Отправить сообщение

Первый вопрос: а какая точность по времени у выхода GNSS приёмника? В смысле какая максимальная гарантированная разница между таймингами прихода импульсов от этих конкретных моделей приёмников разнесенных на несколько сотен метров?

Точность прихода импульсов у отдельно взятого u-blox NEO-7M 60 наносекунд. Т.е., если я правильно понимаю разработчика, в 99% очередной фронт PPS сдвинут относительно истинного положения (как в атомных часах) максимум на 60 нс. Точность системы в целом не знаю, как посчитать, очень грубо 120 нс для двух приемников, 180 нс - для трех и т.д. Скорее всего, речь идет о величине, которая для 5-6 датчиков не превышает 1 мкс.

Наконец, конечно, там лучше подходит не малинка, а ESP32 и аналоги. Та же поддержка WiFi, но риалтайм из коробки без заморочек с модулями ядра.

А я уже выстроил свою экосистему на малинках) Надеюсь лишь, что какой-нибудь новичок, прочитав статью, зайдет в комментарии и сделает для себя выводы, какое оборудование ему больше подходит. Мне нравится в малинках, что не нужно городить монструозный код-прошивку, и разные задачи можно поручить разным прогам. Да, я писал уже в комментариях, что Wi-Fi у меня не всегда может работать, а запись не прерывается, я просто потом отправляю файлы на сервер через cron и rsync, как появляется сеть.

А какие требования к синхронности?

Поскольку частота маленькая, у меня требование довольно грубое: смещение сигнала по времени на 20 мс (на 1 сэмпл) в одну или в другую сторону неприемлемо. Собственно, долгое время я пользовался кодом на Python, в котором реализовано то, о чем вы говорите - свободные измерения, не привязанные к внешней синхронизации. Существенных помех тогда не было, но и выдерживать нужное число сэмплов в час не получилось (пишу часовые бинарные файлы), т.е. итоговая частота получалась не 50 SPS, а типа 49.95. И при сопоставлении данных с разных регистраторов получалась дичь.

Python-код (старая прога)
#!/usr/bin/python
# coding: utf-8

# Программа чтения данных с микросхемы ADS1256
# и их последующей записи на HDD
# Исходная редакция 27.09.19
# Редакция 26.09.20

# В помощь: https://ph0en1x.net/106-rpi-gpio-installation-working-with-inputs-outputs-interrupts.html
# А также: http://academicfox.com/kak-yspolzovat-preryivanyya-s-python-na-raspberry-pi-y-rpi-gpio/

import os
import RPi.GPIO as GPIO
import time
import spidev
import struct
import datetime as dt

# Основные методы модуля RPi.GPIO
# GPIO.setmode(GPIO.BCM либо GPIO.BOARD)            выбор типа нумерации
# pin_mode = GPIO.getmode()                         получить текущий режим нумерации пинов
# GPIO.setup(pin_number, pin_mode)                  pin_mode - GPIO.IN (1) либо GPIO.OUT (0)
# GPIO.setup(7, GPIO.OUT, initial=GPIO.HIGH)        установка нач. состояния пина на вывод
# GPIO.setup(7, GPIO.IN, pull_up_down=GPIO.PUD_UP)  подтягивание к питанию, к примеру
# GPIO.input(pin_number)                            чтение пина, сконф. на вход
# GPIO.output(pin_number, pin_state)                установка уровня пина на выход
# GPIO.cleanup(pin_number)                          сброс состояний пинов

# Основные методы time
# time.sleep()
# time.time()               время в сек, прошедшее с 01.01.1970

# Основные методы spidev
# spi0 = spidev.SpiDev()        создать новый spi объект
# spi0.open(port, device)
# spi0.max_speed_hz = ...
# spi0.cshigh = True/False      уст. уровня на CS
# spi0.mode = [0b00...0b11]     полярность и фаза

# Основные константы
# Усиление
PGA_GAIN1   = 0 # Input voltage range: +- 5 V
PGA_GAIN2   = 1 # Input voltage range: +- 2.5 V
PGA_GAIN4   = 2 # Input voltage range: +- 1.25 V
PGA_GAIN8   = 3 # Input voltage range: +- 0.625 V
PGA_GAIN16  = 4 # Input voltage range: +- 0.3125 V
PGA_GAIN32  = 5 # Input voltage range: +- 0.15625 V
PGA_GAIN64  = 6 # Input voltage range: +- 0.078125 V
# data rate
DRATE_30000 = 0xF0 
DRATE_15000 = 0xE0
DRATE_7500  = 0xD0
DRATE_3750  = 0xC0
DRATE_2000  = 0xB0
DRATE_1000  = 0xA1
DRATE_500   = 0x92
DRATE_100   = 0x82
DRATE_60    = 0x72
DRATE_50    = 0x63
DRATE_30    = 0x53
DRATE_25    = 0x43
DRATE_15    = 0x33
DRATE_10    = 0x20
DRATE_5     = 0x13
DRATE_2d5   = 0x03
# Регистры
REG_STATUS = 0   # Register adress: 00h, Reset value: x1H
REG_MUX    = 1  # Register adress: 01h, Reset value: 01H
REG_ADCON  = 2  # Register adress: 02h, Reset value: 20H
REG_DRATE  = 3  # Register adress: 03h, Reset value: F0H
REG_IO     = 4  # Register adress: 04h, Reset value: E0H
REG_OFC0   = 5  # Register adress: 05h, Reset value: xxH
REG_OFC1   = 6  # Register adress: 06h, Reset value: xxH
REG_OFC2   = 7  # Register adress: 07h, Reset value: xxH
REG_FSC0   = 8  # Register adress: 08h, Reset value: xxH
REG_FSC1   = 9  # Register adress: 09h, Reset value: xxH
REG_FSC2   = 10 # Register adress: 0Ah, Reset value: xxH
# Команды
CMD_WAKEUP   = 0x00 # Completes SYNC and Exits Standby Mode
CMD_RDATA    = 0x01 # Read Data
CMD_RDATAC   = 0x03 # Read Data Continuously
CMD_SDATAC   = 0x0F # Stop Read Data Continuously
CMD_RREG     = 0x10 # Read from REG - 1st command byte: 0001rrrr 
                    #                   2nd command byte: 0000nnnn
CMD_WREG     = 0x50 # Write to REG  - 1st command byte: 0001rrrr
                    #                   2nd command byte: 0000nnnn
                    # r = starting reg address, n = number of reg addresses
CMD_SELFCAL  = 0xF0 # Offset and Gain Self-Calibration
CMD_SELFOCAL = 0xF1 # Offset Self-Calibration
CMD_SELFGCAL = 0xF2 # Gain Self-Calibration
CMD_SYSOCAL  = 0xF3 # System Offset Calibration
CMD_SYSGCAL  = 0xF4 # System Gain Calibration
CMD_SYNC     = 0xFC # Synchronize the A/D Conversion
CMD_STANDBY  = 0xFD # Begin Standby Mode
CMD_RESET    = 0xFE # Reset to Power-Up Values
# Аналоговые входы
AIN0   = 0 # Binary value: 0000 0000
AIN1   = 1 # Binary value: 0000 0001
AIN2   = 2 # Binary value: 0000 0010
AIN3   = 3 # Binary value: 0000 0011
AIN4   = 4 # Binary value: 0000 0100
AIN5   = 5 # Binary value: 0000 0101
AIN6   = 6 # Binary value: 0000 0110
AIN7   = 7 # Binary value: 0000 0111

# Основные задействованные пины
# DRDY - 11(bcm), 17(py)
# RST  - 12(bcm), 18(py)
# CS   - 15(bcm), 22(py)
# DIN  - 19(bcm), 10(py)
# DOUT - 21(bcm), 9(py)
# SCLK - 23(bcm), 11(py)

# Функция задержки в мкс
def delayus(us):
    time.sleep(us*10**-6)
# end

# Функция прерывания по событию
# падение с 1 на 0 на DRDY
def interrupting():
    GPIO.wait_for_edge(17, GPIO.FALLING)
# end

# Запись в регистр
def writereg(regID, value):
    interrupting()
    GPIO.output(22, GPIO.LOW)
    spi0.writebytes([CMD_WREG | regID])
    spi0.writebytes([0x00])
    spi0.writebytes([value])
    GPIO.output(22, GPIO.HIGH)
#end

# Основная функция
# Массив параметров регистров
regBuf = []

# Инициализация и настройка SPI, CS, DRDY
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
spi0 = spidev.SpiDev()
spi0.open(0, 0)
spi0.max_speed_hz = 976000
spi0.mode = 0b01
GPIO.setup(22, GPIO.OUT)
spi0.cshigh = True
GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP)

# Настройка управляющего скриптом пина 13(bcm) -> 27(py)
# GPIO.setup(27, GPIO.IN, initial=GPIO.LOW)

# Сброс регистров к дефолту
spi0.writebytes([CMD_RESET])
delayus(10)

# Настройка регистров
# автокалибровка вкл, остальное по дефолту
regBuf.append(0 << 3 | 1 << 2 | 0 << 1)
# analog input positive (AINP) = AIN1, AINN = AINCOM (земля, фактич.)
regBuf.append(AIN1 << 4 | AIN0)#1 << 3)
# внеш. тактирование выкл, сенсоры выкл, усиления нет
regBuf.append(0 << 5 | 0 << 3 | PGA_GAIN2)
# частота оцифровки 50 SPS
regBuf.append(DRATE_50)
# GPIO пины отключены
regBuf.append(0x00)

# Внесение данных в регистры
writereg(REG_STATUS, regBuf[0])
writereg(REG_MUX, regBuf[1])
writereg(REG_ADCON, regBuf[2])
writereg(REG_DRATE, regBuf[3])
writereg(REG_IO, regBuf[4])
delayus(50)

# Основная директория:
main_dir = '/home/pi/Documents/ADC'
os.chdir(main_dir)

# Время начала отсчета
base = dt.datetime(1970,1,1)

try:
    # Бесконечное создание новых файлов
    while True:
        # Возврат в главную директорию
        os.chdir(main_dir)
        # Дата и время начала записи файла
        now = dt.datetime.now()
        # Текущий год (4 цифры)
        now_year = now.strftime('%Y')
        if os.path.exists(now_year) != True:
            os.mkdir(now_year)
            os.chdir(now_year)
        else:
            os.chdir(now_year)
        # Текущий месяц (дополненный нулем)
        now_month = now.strftime('%m')
        if os.path.exists(now_month) != True:
            os.mkdir(now_month)
            os.chdir(now_month)
        else:
            os.chdir(now_month)
        # Текущий день (дополненный нулем)
        now_day = now.strftime('%d')
        if os.path.exists(now_day) != True:
            os.mkdir(now_day)
            os.chdir(now_day)
        else:
            os.chdir(now_day)
        # Создание нового файла в директории типа "день"
        # Если nomatter.bin уже существует
        if os.path.exists('nomatter.bin') == True:
            if os.path.getsize('nomatter.bin') != 0:
                f = open('nomatter.bin','rb')
                #f.seek(4)
                data1 = f.read(8)
                f.close()
                data2 = struct.unpack('d',data1)
                data2 = base + dt.timedelta(seconds=data2[0])
                new_name = data2.strftime("%Y-%m-%d-%H-%M-%S")+'.bin'
                os.rename('nomatter.bin', new_name)
            else: continue
        # Если не существует
        try:
            f = open('nomatter.bin','ab')
        except IOError:
            print('IOError!')
            break
        # Формат данных: беззнаковый int
        # format = 'I'
        # Формат данных: float32
        # format = 'f'
        # Формат данных: double64
        # format = 'd'
        
        # Оцифровка в непрерывном режиме
        start = time.time()
        data = struct.pack('d', time.time())
        f.write(data)
        end = start
        count = 0
        # На CS устанавливаем лог. '0'
        GPIO.output(22, GPIO.LOW)
        interrupting()
        spi0.writebytes([CMD_RDATAC])
        # Ожидание не менее 6.5 мкс
        delayus(10)
        # Запись файла в течение 1ч
        while (end-start)<=3600:
            interrupting()
            dataBuf = spi0.readbytes(3)
            # Получение временного отсчета
            #timeValue = time.time()
            # Конструирование 24-битного значения
            adcValue = dataBuf[0] << 16
            adcValue |= dataBuf[1] << 8
            adcValue |= dataBuf[2]
            if adcValue & 0x800000: adcValue |= 0xFF000000
            else: adcValue |= 0x00000000
            
            f.seek(8+count*4)
            data = struct.pack('I', adcValue)
            f.write(data)
            #data = struct.pack('d', timeValue)
            #f.write(data)
            
            end = time.time()
            count = count+1
        # end while
        interrupting()
        spi0.writebytes([CMD_SDATAC])
        delayus(10)
        f.close()
        # Переименование файла
        new_name = now.strftime("%Y-%m-%d-%H-%M-%S") +'.bin'
        os.rename('nomatter.bin', new_name)
    # end while
except KeyboardInterrupt:
    interrupting()
    spi0.writebytes([CMD_SDATAC])
    delayus(10)
    f.close()
    # Закрытие SPI и сброс пинов к дефолту
    GPIO.output(22, GPIO.HIGH)
    spi0.close()
    GPIO.cleanup(17)
    GPIO.cleanup(22)
    # GPIO.cleanup(27)
# return 0

У меня акустическая станция, где стоят эти малинки. Синхронность работы проверяю, взрывая петарды на территории. Пришел, взорвал, чекнул геолокацию, потом уже по задержкам приема пытаюсь восстановить локацию. Сейчас всё гуд, может быть, напишу об этом как-нибудь. Помеха не влияет на задержки времени, но портит амплитуду, о чем я и пишу.

вам видимо не о чем не говорит

Конечно не говорит, вы же так сумбурно пишите, уахахахаха)))

частота импульсов и период отдельного импульса, как вы их задаете, как вы ими управляете?

50 Гц частота SYNC и период импульса 1 мкс, это есть в статье. Вы точно читали текст?

чтобы это время не превышало времени экспозиции очередного значения АЦП

Эх, новое преобразование не идет, пока не извлечешь данные, об этом я тоже писал.

вы не пробовали такой режим включать-использовать

Попробовать можно.

То что у вас в конце написано тоже довольно сумбурно выглядит как попытка самостоятельно вывести теорему Котельникова-Найквиста

А чего вы не процитировали, как вы это обычно делаете, что я чего-то нового вывожу? Мне тоже любопытно посмотреть, где это у меня в тексте.

частота 50гц "на столе" будет проникать со всех сторон

Хорошо, а почему помеха проявляет себя даже в том случае, когда нет никаких 50 Гц? Когда питание от аккумулятора через DC-DC преобразователь. Я об этом не пишу напрямую, но я всё-таки добавил про "не зависит от источника питания".

По идее u-blox модули и сами имют PLL и выдавать до 10Мгц на выходе

Про эту фишку знаю. Там вся сложность в том, что, хорошо, u-blox теперь тактирует АЦП вместо кварца. А как отследить момент, когда он начал тактировать? У ублоксов момент переключения частот слегка отвратительно реализован. И ведь потом надо когда как-то считать эти тики 7.68М, чтобы не потерять привязку к моменту переключения частот (выраженному в чч:мм:сс), в полевых условиях, когда регистрация должна запускаться автоматически, это непросто. Так что, я считаю, что пошел по пути наименьшего сопротивления. И, повторюсь, синхронность отрабатывается на все деньги, и мне уже не важно, что ради этого пришлось пойти на сложности с GNSS и пр. Подробности я могу в личку написать.

Ну и самое главное, RPI и и линукс тут явно избыточны

Согласен. Единственное, имхо (повторюсь: опыта немного, могу ошибаться), работа с физическими носителями в ESP32/Ардуине - тихий ужас. А у меня часто бывает, что пропадает Wi-Fi, но малинка и в таком случае продолжает писать в файл на флэшку. Я спорить не буду, это "ошибка молодости", но у меня в обсерватории пять комплектов регистраторов на Raspberry работает, не вижу смысла начинать с чистого листа.

Благодарю за инфу! Мне тоже больше нравится такое решение, когда АЦП живет своей жизнью, а время каждого преобразования вычисляется внешними средствами, Я уже писал в комментариях, что GNSS может измерять время внешнего сигнала, таким сигналом как раз может быть флаг готовности DRDY. Конечно, чтобы потом сводить данные с разных регистраторов, придется лишний раз делать интерполяцию, но это уже не так страшно.

В работе, кстати, готовая плата используется, ссылка. Да, кстати, решение было довольно спонтанное и не самое обдуманное, гнаться за разрядностью не стоило. Шумы самого датчика (не считая помехи, описанной в статье) с лихвой перекрывают эти микровольты. Я лишь показываю, что АЦП может цифровать без дополнительной помехи, но почему-то этого не делает в режиме синхронизации от внешнего источника.

Как связаны времена обращений по SPI с временами запуска АЦП

Начать стоит с того, что при включении АЦП она работает на базовой частоте 30,000 SPS, нужно записать в регистры новую частоту, аналоговые входы для считывания, PGA.

Картинка номер 2, SYNC - это вход микросхемы, DRDY - выход. Пока DRDY в 1 - идет очередное преобразование, его результат извлекается после смены уровня на DRDY. Параметры SPI соединения нужно настроить заранее, отправляется команда RDATA (это всего 1 байт 0х01), после этого нужно подождать, пока АЦП по SPI вернёт 3 байта. На этом базисе основан код, который я предлагаю в статье.

Минпромторг данному НИИ дозволил закупать импортные микросхемы?

Пока что да) Но скоро лавочку прикроют, действительно.

600 усреднений делается внутри микросхемы, нужно просто дискретизацию увеличить, на частоте 500 SPS их будет уже 60. Я примерно о таком и пишу, что да, можно после строба SYNC получить несколько значений на более высокой частоте, и усреднить их уже программно. Но мне не нравится этот путь, поскольку АЦП и так должен выплюнуть готовое значение, которое не нуждается в постобработке. А я хотел бы в перспективе подключать к одному АЦП несколько разных датчиков, код тогда будет совсем перегруженным. Лучше, конечно, устранить первопричину помехи, а не пытаться под нее приспособиться.

Хм, у меня в документации только указано, что синхронизация случится на первом же такте CLKIN, после того, как SYNC перейдет из 0 в 1, т.е. как будто требование не такое жесткое. Но это интересный момент, можно его поисследовать.

Интересно, спасибо! Насколько я понял, не нужно настраивать регистр DRATE в АЦП: частота АЦП при включении 30 кГц и мы ее не трогаем. Перестроиться на любую частоту можно буквально одной командой (у меня так сделать нельзя, всё жестко прописано). Технически, можно даже определять точное время каждого импульса, GNSS-приемники дают такой функционал (раздел 14. Timemark). Я, получается, в своей работе пошел от обратного, я сначала настраиваю GNSS-приемник на импульсный сигнал определенной частоты, а уже он управляет преобразованием. Я так могу легко сопоставлять между собой записи с разных регистраторов, даже удаленных друг от друга, и они реально работают синхронно (не знаю, смогу ли опубликовать пост, как проверяю синхронность).

Нет, то, о чем вы говорите, я даже не рассматривал. Сначала я подумал, что речь про отправку команд по SPI через ioctl, но, кажется, это что-то совсем другое, я обязательно про это прочитаю. На первый взгляд, input-подсистема включает примерно те же вещи, что я пытался внедрять в свой модуль, например, на ранних этапах я пробовал скачивать буфер данных из ядра через "файл" в /proc.

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность

Специализация

Десктоп разработчик, Инженер встраиваемых систем
От 100 000 ₽
Python
Linux
MySQL
Bash