Karat3.ru

Клуб каратистов
Текущее время: 14 май 2026, 02:02

Часовой пояс: UTC




Начать новую тему  Ответить на тему  [ 44 сообщения ]  На страницу Пред. 1 2 3 4 5 След.
Автор Сообщение
 Заголовок сообщения: Re: RP2040
СообщениеДобавлено: 05 апр 2026, 09:10 
Не в сети

Зарегистрирован: 25 ноя 2024, 13:52
Сообщения: 97
Теперь, надо бы при приёме записанные кортежи выводить в виде txt-файла.

Работа с файловой системой в CircuitPython имеет важную особенность: по умолчанию для скрипта она доступна только для чтения.
Чтобы скрипт мог сам записывать данные в файл, нужно:
- Создать файл boot.py в корне диска (если его нет).
- Добавить туда код, разрешающий запись.
- Перезагрузить устройство.
Но при этом вы потеряете возможность записывать на флэш с компьютера через USB.
Код boot.py разрешающий запись для скрипта и запрещающий запись для компьютера:
Код:
import storage
storage.remount("/", False) 
Чтобы снова иметь возможность удалять или редактировать файлы на диске через компьютер, вам нужно будет закомментировать строку в boot.py или использовать переключатель на пине.

Код boot.py с использованием переключателя на пине GP0.
Код:
import storage
import board
import digitalio

# Настраиваем GP0 как вход с подтяжкой к ПЛЮСУ (High)
switch = digitalio.DigitalInOut(board.GP0)
switch.direction = digitalio.Direction.INPUT
switch.pull = digitalio.Pull.UP

# Если соединить GP0 с землей (GND), switch.value станет False
# В этом случае ПЛАТА сможет писать файлы, а КОМПЬЮТЕР — нет.
if not switch.value:
    storage.remount("/", False) 
В этом случае, если на пине GP0 логический 0 (земля), запись разрешена скрипту,
если пин GP0 свободен - запись разрешена компьютеру.
Это "предохранитель", чтобы вы могли вернуть доступ по USB.

Запишем файл boot.py
Вложение:
boot.rar [678 байт]
2 скачивания
.
Файл, определяющий доступность флэша для записи из скрипта:
Вложение:
code_RW.rar [529 байт]
2 скачивания
.
И файл, создающий и записывающий данные в txt-файл
Вложение:
code58.rar [1.25 КБ]
2 скачивания
Создаётся файл rtty_data.txt и в него записываются данные о принятых RTTY-битах
.
Важное предупреждение:
Никогда не вытаскивайте USB-кабель и не жмите Reset в момент, когда идет активная запись в файл (log_file.flush()). Это может повредить файловую систему (FAT) на флешке контроллера.

P.S.
Можно из командной строки удалить файл boot.py
Для этого:
- Откройте консольное окно, нажмите Ctrl+C для остановки программы
- введите последовательно
import os
os.remove("/boot.py")
- Нажмите кнопку Reset. Теперь диск снова станет обычным «флеш-накопителем».


Вернуться к началу
 Заголовок сообщения: Re: RP2040
СообщениеДобавлено: 05 апр 2026, 09:26 
Не в сети

Зарегистрирован: 25 ноя 2024, 13:52
Сообщения: 97
Посмотрим, что у нас захватывает плата.
В файл выводить пока не буду, но запущу фильтрацию с детектором переходов.

Код
Вложение:
code59.rar [1.29 КБ]
2 скачивания
.
Данные в консоли (справа) сравним с данными захвата (слева)
Вложение:
09.xlsx [8.66 КБ]
2 скачивания
.
Первая единица - это пилот-тон. Его длительность можно не брать во внимание.
Вложение:
09.jpg
09.jpg [ 152.16 КБ | 193 просмотра ]
.
Вроде похоже, но длительности определены неточно.

Попробуем добавить RTTY-декодер
Вложение:
code63.rar [2.15 КБ]
2 скачивания
.
Раскодируется с ошибкой
Вложение:
63.jpg
63.jpg [ 193.43 КБ | 190 просмотров ]


Вернуться к началу
 Заголовок сообщения: Re: RP2040
СообщениеДобавлено: 06 апр 2026, 07:52 
Не в сети

Зарегистрирован: 25 ноя 2024, 13:52
Сообщения: 97
Заманал меня CircuitPython: быстро не работает, одно ядро всегда занято интерпретатором, память занята им же...
Очень сильно похоже на работу обычных Raspberry Pi.

А мне надо ближе к "железу", чтоб не было "зависонов" и неконтролируемых пауз.
Попробую поработать в МикроПитоне.
Он примерно такой же как CircuitPython, но говорят, что позволяет полнее контролировать регистры.

1. Качаем thonny-4.1.7-windows-portable
https://disk.yandex.ru/d/CCjsnynFOFQneQ

2. Сбрасываем плату в boot-режим (RST+BOOT, отпустить RST, отпустить BOOT)

Можно сразу записать в плату эту прошивку
https://disk.yandex.ru/d/CCjsnynFOFQneQ
Вложение:
RPI_PICO-20251209-v1.27.0.rar [242 КБ]
2 скачивания
а можно сделать это через Thonny

3. Распаковываем и запускаем Thonny.
Естественно пока ничего нет и плата не находится.
Открываем
Выполнить -> Настроить интерпретатор
Вложение:
01.png
01.png [ 164.45 КБ | 179 просмотров ]
.
4. Выбираем МикроПитон для обычной платы (Пи Пико)
Вложение:
02.png
02.png [ 154.4 КБ | 179 просмотров ]
.
5. Пытаемся найти плату автоматически (не найдёт, т.к. в плату не загружена прошивка)
Вложение:
03.png
03.png [ 170.16 КБ | 179 просмотров ]
.
6. Нажимаем "Установить или обновить МикроПитон"
Пустая плата видится как RPI-RP2.
Выбираем версию для Пи Пико. Последняя версия 1.27.0
Вложение:
04.png
04.png [ 267.29 КБ | 179 просмотров ]
.
7. Нажимаем "Установить"
Процесс идёт
Вложение:
05.png
05.png [ 327.1 КБ | 179 просмотров ]
.
8. Когда видим, что прошивка скачалась, установилась и нашёлся порт - значит всё ок, можно закрывать
Вложение:
06.png
06.png [ 372.44 КБ | 179 просмотров ]
.
9. Выходим в основное окно программы - внизу видим, что плата успешно определилась
Вложение:
07.png
07.png [ 88.96 КБ | 179 просмотров ]
.
Можно зайти обратно в настройки интерпретатора и убедиться, что плата найдена, в моём случае, на СОМ8
Вложение:
08.png
08.png [ 225.8 КБ | 179 просмотров ]


Вернуться к началу
 Заголовок сообщения: Re: RP2040
СообщениеДобавлено: 06 апр 2026, 08:34 
Не в сети

Зарегистрирован: 25 ноя 2024, 13:52
Сообщения: 97
Нет, отложим пока в сторону - в микропитоне всё ещё хуже...

Что за хня?!
В СёркуитПайтоне нет доступа к регистрам АЦП. Нельзя запустить Free-running mode.
Зато есть ulab которая позволяет быстро обрабатывать блоки данных.

В МикроПитоне есть доступ к регистрам (вроде бы).
Но нет ulab и все данные надо гонять вручную или очень медленно.

Вот прям заставляют писать на Си.
Но у Си нет удобной оболочки... Нужно скомпилировать под себя конкретную среду и работать в консоли... Аж бесит.


Вернуться к началу
 Заголовок сообщения: Re: RP2040
СообщениеДобавлено: 07 апр 2026, 02:52 
Не в сети

Зарегистрирован: 25 ноя 2024, 13:52
Сообщения: 97
Так, немного выдохнул :D
Есть же хорошая среда Си для RP2040 - это Ардуино!

1. Качаем последнюю ардуину. Запускаем.
https://disk.yandex.ru/d/79vBGEyiCxqBJw

Нужно установить поддержку плат Pico
2. Файл -> Параметры
Вложение:
01.jpg
01.jpg [ 133.12 КБ | 167 просмотров ]
.
Дополнительные ссылки для менеджера плат.
https://github.com/earlephilhower/ardui ... index.json
Вложение:
02.jpg
02.jpg [ 196.11 КБ | 167 просмотров ]
.
3. Инструменты -> плата -> Менеджер плат
у меня уже установлено, но покажу
Вложение:
03.jpg
03.jpg [ 181.25 КБ | 167 просмотров ]
.
набираем pico
Устанавливаем
Raspberry Pi Pico/RP2040/RP2350 от Earle F. Philhower, III
Вложение:
04.jpg
04.jpg [ 271.87 КБ | 167 просмотров ]
.
4. Выбираем плату
Вложение:
05.jpg
05.jpg [ 596.17 КБ | 167 просмотров ]
.
Выбираем порт (плата должна быть подключена и сброшена RST-BOOT)
Вложение:
06.jpg
06.jpg [ 276.68 КБ | 167 просмотров ]
.
5. Загрузим тестовый скетч (мигание встроенным светодиодом)
Код:
void setup() {
  // Инициализируем встроенный светодиод как выход
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH); // Включаем (3.3V)
  delay(500);                      // Ждем полсекунды
  digitalWrite(LED_BUILTIN, LOW);  // Выключаем (0V)
  delay(500);                      // Ждем полсекунды
}
В правом нижнем углу видим - плата найдена и подключилась
Вложение:
07.jpg
07.jpg [ 156.77 КБ | 167 просмотров ]
.
6. Загружаем скетч на плату
Вложение:
08.jpg
08.jpg [ 147.91 КБ | 167 просмотров ]
.
Он компилируется и записывается на плату.
Нужно отметить, что после записи плата начинает выполнять код и становится полностью недоступна.
Для повторной записи на плату, её нужно опять сбросить RST-BOOT.
Вложение:
09.jpg
09.jpg [ 253.33 КБ | 167 просмотров ]
.
P.S.
После первой записи скетча на плату нужно по-новой переназначить порт.
Тогда не нужно каждый раз сбрасывать RST-BOOT. Arduino IDE пишет свободно.
Вложение:
10.jpg
10.jpg [ 577.02 КБ | 156 просмотров ]
.
Также в Файл-Параметры можно отметить подробный вывод комментариев при компиляции.
Можно проследить, куда Ардуино создаёт UF2-файлы.


Вернуться к началу
 Заголовок сообщения: Re: RP2040
СообщениеДобавлено: 07 апр 2026, 03:28 
Не в сети

Зарегистрирован: 25 ноя 2024, 13:52
Сообщения: 97
Какая просматривается идеология:
частота дискретизации 8 Кс/с
- запускаем АЦП на чтение по прерываниям каждые 1/8000 = 125 мкс,
- складываем полученные значения в буфер А,
- как только набралось 8 отсчётов, меняем буфер - начинаем писать в буфер Б, и прерыванием вызываем обработку буфера А,
- потом меняем буфер и т.д.

Обработка такая:
- есть буфер CHUNK принятого сигнала длиной 8000*0,022 = 176 отсчётов,
- при каждом прерывании по заполнению буфера А/Б выкидываем из CHUNK самые старые 8 отсчётов и добавляем в начало вновь принятые,
- пересчитываем буфер CHUNK алгоритмом Гёрцеля на две частоты (МАРК и СПЕЙС),
- сравниваем полученные энергии с пороговым значением и между собой,
если энергия хотя бы одного сигнала выше порога, тогда сравниваем энергии МАРК и СПЕЙС между собой - назначаем 1 или 0.
если обе энергии ниже уровня порога - это шум, не делаем ничего.

Это называется скользящее окно Гёрцеля.
Позволяет быстро обнаружить переход от одной частоты к другой - т.е. синхронизироваться с фронтами импульсов.
Крутизна фронтов фильтра в наших условиях хорошая.
Частотный бин (шаг спектрального разрешения) 8000 / 176 = 45,45 Гц
у нас разнос частот 170 Гц, это почти 4 бина.

На графиках это выглядит примерно так
Вложение:
граф_01.png
граф_01.png [ 142.69 КБ | 166 просмотров ]
.
соседние лепестки подавлены примерно на -13 дБ. Немного.

Чтобы справиться с "лепестками" (это явление называется утечка спектра) применим оконные функции.
Оконные функции давят лепестки, но при этом расширяется полоса самого фильтра.
Например, в нашем случае применение оконной функции Ганна (Hanning) эквивалентно вычислению частот двух соседних бинов.
Если без оконной функции (или ещё говорят с прямоугольной оконной функцией) у нас ширина фильтра Гёрцеля равна 2 бинам,
т.е. по нулевым точкам основания ширина была равна 2*45,45 = 90,9 Гц,
то при использовании оконной функции Ганна эта ширина станет вдвое больше - 4 бина или 4*45,45 = 181,8 Гц.
Вложение:
граф_02.png
граф_02.png [ 90.45 КБ | 162 просмотра ]
.
Оконная функция Хэмминга по ширине полосы похожа на функцию Ганна, но оптимизирована на подавление ближайших лепестков,
при этом дальние лепестки подавлены хуже, чем у Ганна.
Вложение:
граф_03.png
граф_03.png [ 118.3 КБ | 160 просмотров ]
.
Оконная функция Блэкмана имеет бОльшее подавление, но и бОльшую ширину полосы
Вложение:
граф_04.png
граф_04.png [ 167.53 КБ | 160 просмотров ]
.
Думаю, для наших условий оптимально будет окно Хэмминга.

По уровню -3 дБ:
- прямоугольное окно 0,89*бин = 40,5 Гц,
- окно Ганна 1,44*бин = 65,5 Гц,
- окно Хэмминга 1,30*бин = 59 Гц,
- окно Блэкмана 1,64*бин = 74,5 Гц.

Уровни на пиках характеристик фильтров:
- прямоугольное окно 0 дБ,
- окно Ганна -6,02 дБ,
- окно Хэмминга -5,35 дБ,
- окно Блэкмана -7,54 дБ.

Уровни на пересечении характеристик фильтров:
- прямоугольное окно -18,5 дБ,
- окно Ганна -42,2 дБ,
- окно Хэмминга -43,5 дБ,
- окно Блэкмана -39,1 дБ.

При этом максимальные уровни лепестков:
- прямоугольное окно -13 дБ,
- окно Ганна -31,5 дБ,
- окно Хэмминга -43 дБ,
- окно Блэкмана -58 дБ.

Если сравнить графики по уровню разделения каналов, то получим
Вложение:
граф_05.png
граф_05.png [ 235.07 КБ | 159 просмотров ]
.
Синий - прямоугольное окно в точке пересечения имеет неплохое подавление, но огромные соседние лепестки портят всю картину.
Разделения лучше -13 дБ не получить.

Красный - окно Ганна разделение неплохое -31 дБ, примерно равно соседнему лепестку

Фиолетовый - окно Блэкмана отлично давит в дальней зоне, но на пересечении характеристик даёт всего -17 дБ, что ненамного лучше прямоугольного окна.

Зелёный - окно Хэмминга разделяет каналы на -37 дБ, для данных условий это лучшая оконная функция.

Положу тут реализацию оконной функции Хэмминга на Си
Код:
#include <stdio.h>
#include <math.h>

#define N 176
#define M_PI 3.14159265358979323846

// Массив для хранения коэффициентов окна
float hamming_window[N];

// 1. Инициализация окна (выполняется один раз при старте)
void init_hamming_window() {
    for (int n = 0; n < N; n++) {
        // Формула: 0.54 - 0.46 * cos(2 * PI * n / (N - 1))
        hamming_window[n] = 0.54f - 0.46f * cosf(2.0f * M_PI * n / (N - 1));
    }
}

// 2. Применение окна к буферу данных (выполняется в реальном времени)
void apply_window(float* signal_buffer) {
    for (int n = 0; n < N; n++) {
        signal_buffer[n] *= hamming_window[n];
    }
}

int main() {
    // Пример входных данных (синусоида)
    float signal[N];
    for(int i = 0; i < N; i++) signal[i] = 1.0f; // Условно постоянный сигнал

    init_hamming_window();
    apply_window(signal);

    // Вывод первых 5 значений для проверки
    printf("Первые 5 отсчетов окна Хэмминга:\n");
    for (int i = 0; i < 5; i++) {
        printf("w[%d] = %.4f\n", i, hamming_window[i]);
    }

    return 0;
}


Вернуться к началу
 Заголовок сообщения: Re: RP2040
СообщениеДобавлено: 07 апр 2026, 14:02 
Не в сети

Зарегистрирован: 25 ноя 2024, 13:52
Сообщения: 97
Вот код на си, который реализует алгоритм Гёрцеля со скользящим окном и оконной функцией Хэмминга.
Код:
#include <Arduino.h>
#include <hardware/adc.h>
#include <hardware/dma.h>
#include <hardware/irq.h>
#include <hardware/pio.h>
#include <hardware/clocks.h>

#define ADC_PIN 27
#define RTTY_OUT_PIN 15
#define ADC_NUM 1
#define FS 8000.0f
#define N 176
#define STEP 8
#define F1 1000.0f
#define F2 1170.0f
#define FN 1085.0f
#define Q_COEFF 14
#define Q_WIN 15
#define SNR_RATIO 16
#define CONFIRM_COUNT 5
#define MISS_COUNT 3
#define MIN_POWER 1000000ULL

int16_t hamming_win[N];
int32_t coeff_f1, coeff_f2, coeff_fn;
volatile uint16_t adc_raw_ring[N];
int dma_chan;
dma_channel_config dma_cfg;
volatile bool data_ready = false;
bool f1_active = false, f2_active = false;
int8_t f1_cnt = 0, f2_cnt = 0;

void init_dsp() {
    const float pi = 3.1415926535f;
    coeff_f1 = (int32_t)(2.0f * cosf(2.0f * pi * F1 / FS) * (1 << Q_COEFF));
    coeff_f2 = (int32_t)(2.0f * cosf(2.0f * pi * F2 / FS) * (1 << Q_COEFF));
    coeff_fn = (int32_t)(2.0f * cosf(2.0f * pi * FN / FS) * (1 << Q_COEFF));
    for (int n = 0; n < N; n++) {
        float w = 0.54f - 0.46f * cosf(2.0f * pi * n / (N - 1));
        hamming_win[n] = (int16_t)(w * 32767);
    }
}

uint64_t calculate_goertzel(int start_ptr, int32_t coeff) {
    int32_t q0, q1 = 0, q2 = 0;
    for (int i = 0; i < N; i++) {
        int idx = (start_ptr + i) % N;
        int32_t sample = (int32_t)adc_raw_ring[idx] - 2048;
        int32_t x_w = (sample * hamming_win[i]) >> Q_WIN;
        q0 = x_w + ((coeff * q1) >> Q_COEFF) - q2;
        q2 = q1;
        q1 = q0;
    }
    int64_t t1 = (int64_t)q1 * q1;
    int64_t t2 = (int64_t)q2 * q2;
    int64_t t3 = ((int64_t)q1 * q2 * (int64_t)coeff) >> Q_COEFF;
    return (uint64_t)(t1 + t2 - t3);
}

void __isr dma_handler() {
    dma_hw->ints0 = 1u << dma_chan;
    data_ready = true;
    static int current_write_pos = 0;          
    current_write_pos = (current_write_pos + STEP) % N;
    dma_channel_set_write_addr(dma_chan, &adc_raw_ring[current_write_pos], true);
}

void setup() {
    Serial.begin(115200);
    init_dsp();
    adc_init();
    adc_gpio_init(ADC_PIN);
    adc_select_input(ADC_NUM);
    adc_fifo_setup(true, true, 1, false, false);
    adc_set_clkdiv(6000);
    dma_chan = dma_claim_unused_channel(true);
    dma_cfg = dma_channel_get_default_config(dma_chan);
    channel_config_set_transfer_data_size(&dma_cfg, DMA_SIZE_16);
    channel_config_set_read_increment(&dma_cfg, false);
    channel_config_set_write_increment(&dma_cfg, true);
    channel_config_set_dreq(&dma_cfg, DREQ_ADC);
    dma_channel_set_irq0_enabled(dma_chan, true);
    irq_set_exclusive_handler(DMA_IRQ_0, dma_handler);
    irq_set_enabled(DMA_IRQ_0, true);
    dma_channel_configure(dma_chan, &dma_cfg, &adc_raw_ring[0], &adc_hw->fifo, STEP, true);
    adc_run(true);
    Serial.println("Система запущена: ADC->DMA->Hamming->Goertzel + PIO RTTY");
}

void loop() {
    if (data_ready) {
        data_ready = false;
        static int head = 0;
        uint64_t p1 = calculate_goertzel(head, coeff_f1);
        uint64_t p2 = calculate_goertzel(head, coeff_f2);
        uint64_t pn = calculate_goertzel(head, coeff_fn);
        head = (head + STEP) % N;
        uint64_t threshold = (pn << 4) + MIN_POWER;
        if (p1 > threshold && p1 > (p2 << 1)) {
            if (f1_cnt < CONFIRM_COUNT) f1_cnt++;
        } else {
            if (f1_cnt > -MISS_COUNT) f1_cnt--;
        }
        f1_active = (f1_cnt >= CONFIRM_COUNT);
        if (p2 > threshold && p2 > (p1 << 1)) {
            if (f2_cnt < CONFIRM_COUNT) f2_cnt++;
        } else {
            if (f2_cnt > -MISS_COUNT) f2_cnt--;
        }
        f2_active = (f2_cnt >= CONFIRM_COUNT);
        Serial.print("Mark:");  Serial.print((double)p1/ 1000.0); Serial.print(" ");
        Serial.print("Space:"); Serial.print((double)p2/ 1000.0); Serial.print(" ");
        Serial.print("Noise:"); Serial.println((double)pn/ 1000.0);
    }
}

Разделение каналов = 8750 раз по мощности при размахе 3 В, т.е. 39 дБ что близко к теоретическому пределу при данных условиях.
.
Вложение:
ADC_PIO_plotter.rar [56.14 КБ]
3 скачивания
.
архив содержит uf2-файл, скопировав который на флэш-диск платы получаем установленную программу, которая выводит данные об энергии частот 1000, 1170 и 1085 Гц в порт на скорости 115200.

Вот так выглядит теоретический график АЧХ этого фильтра:
- пик графика Хэмминга ниже на 5,1 дБ по сравнению с прямоугольным окном (т.е. совсем без оконных функций),
- зато подавление боковых и остальных лепестков больше, до 44,3-5,1 = 39,2 дБ (что и подтвердили практические испытания),
- по границе разделения частот (1085 Гц) уровень подавления 36,4-5,1 = 31,3 дБ.
.
Вложение:
Хэмминг_нормированный_с уровнями.png
Хэмминг_нормированный_с уровнями.png [ 116.89 КБ | 145 просмотров ]
.
Напомню, эти графики построены для условий
N=176, Fs=8000, F1=1000, F2=1170.

P.S.
Есть вариант использовать окно Кайзера.
Это универсальный вариант, когда от значения бетта форма функции Кайзера может принимать вид Хэмминга, Ханна, Блэкмана или их суммы.
Например:
Вложение:
Кайзер_разный.png
Кайзер_разный.png [ 170.07 КБ | 136 просмотров ]
.
В сравнении с Хэммингом при бетта 4.8...5.0 достигается наибольше разделение частот. Но страдают боковые лепестки. Крутизна практически та же, так что в данном случае не стоит применять Кайзера.
Вложение:
Хэмминг_Кайзер.png
Хэмминг_Кайзер.png [ 91.89 КБ | 136 просмотров ]


Вернуться к началу
 Заголовок сообщения: Re: RP2040
СообщениеДобавлено: 08 апр 2026, 09:53 
Не в сети

Зарегистрирован: 25 ноя 2024, 13:52
Сообщения: 97
Генератор PIO на 1 кГц.
Вложение:
01.jpg
01.jpg [ 136.76 КБ | 124 просмотра ]
.
Вложение:
1KHZ.rar [731 байт]
6 скачиваний
Оказывается, из под Ардуино не так приятно программировать PIO :)
В ядре от Earle F. Philhower почему-то не исполняются инструкции WARP. Ну да ладно.

На текущий момент стабильная версия генератора + детектора тонов.
.
Вложение:
ADC_PIO_plotter4.rar [2 КБ]
4 скачивания
.
Единственный косяк в том, что при оцифровке меандра полного размаха то ли набегает ошибка, то ли где-то что-то переполняется, каждые 2 секунды по графику пробегает неглубокий провал на 1000 Гц. На 1170 Гц такого нет.


Вернуться к началу
 Заголовок сообщения: Re: RP2040
СообщениеДобавлено: 09 апр 2026, 05:32 
Не в сети

Зарегистрирован: 25 ноя 2024, 13:52
Сообщения: 97
Для кодирования PIO вспоминаем любимый ассемблер :lol:
Ничего страшного, тут всего 9 инструкций:
.
Вложение:
RP2040_PIO_assembler.jpg
RP2040_PIO_assembler.jpg [ 312.43 КБ | 110 просмотров ]
.
кстати, инструкция NOP выглядит как MOV Y,Y = 101 00000 010 00 010 = 0xA042.
101 - инструкция MOV,
00000 - число тактов задержки между данной и следующей инструкциями,
010 - место назначения - регистр Y,
00 - операция над данными - нет (опции: инвертировать, зеркальное отражение порядка бит),
010 - источник данных - регистр Y.

Пока положу это сюда.
Рабочий вариант генератора символов на PIO.
Нужно добавить таблицы, настроить периоды... но сама генерация уже есть.
Вложение:
FSK_MOD_04.rar [2.22 КБ]
2 скачивания


Вернуться к началу
 Заголовок сообщения: Re: RP2040
СообщениеДобавлено: 10 апр 2026, 08:29 
Не в сети

Зарегистрирован: 25 ноя 2024, 13:52
Сообщения: 97
Ещё раз.
Попробую оформить в виде небольших лаб. работ.

Генерируем сигнал при помощи конечного автомата (State Machine).
По умолчанию наши ядра тактируются частотой 125 МГц. От этой же частоты тактируются модули PIO.
К сожалению, делитель 16-разрядный (+8 разрядов дробных), поэтому не получится сделать такты ниже чем 125 МГц / 65 535 = 1907 Гц,
поэтому для начала просто сгенерируем меандр частотой 1 кГц.

В конечный автомат можно вписать небольшую программу, которая будет исполняться независимо от работы ядер центрального процессора.
Пусть наша программа:
- устанавливает вывод пина в 1,
- отсчитывает 50 тактов,
- устанавливает вывод пина в 0,
- отсчитывает ещё 50 тактов,
- повторяет всё снова.

Таким образом, мы получаем меандр.
Посчитаем такты: установить пин - 1 такт, подождать - 49 тактов,
установить пин в 0 - 1 такт, подождать - 48 тактов,
перейти на начало - 1 такт.

Общий период 1+49+1+48+1 = 100 тактов.
Если я хочу получить 1000 Гц за 100 тактов при частоте ядра 125 МГц, то
делитель частоты ядра будет равен 125 000 000 / 100*1000 = 1250.

Проверим,
Fядра = 125 000 000
делитель = 1250
Fpio = 125 000 000 / 1250 = 100 000 Гц.

100 000 Гц / 100 тактов = 1000 Гц.

Всё верно.
Но как установить задержки? В структуре команд PIO есть 5 бит отвечающих за счётчик команд задержки.
Таким образом, дополнительно к одному такту самой команды можно добавить до 31 такта задержки.
А нам нужно 49 и 48.
Придётся или добавлять команду NOP, или просто повторить команду установки/сброса пина.

Программа (адрес:команда)
0: установить вывод пина в 1, пропустить 31 такт,
1: установить вывод пина в 1, пропустить 17 тактов,
2: установить вывод пина в 0, пропустить 31 такт,
3: установить вывод пина в 0, пропустить 16 тактов,
4: перейти на адрес 0

Проверяем такты: 1+31+1+17 = 50, 1+31+1+16+1 = 50.
Должен получиться меандр.
Код:
// генератор 1 кГц на PIO

#include "hardware/pio.h"
#include "hardware/clocks.h"

static const uint16_t blink_instructions[] = {
    0xff01, // 0: set pins, 1 [31]
    0xf101, // 1: set pins, 1 [17]
    0xff00, // 2: set pins, 0 [31]
    0xf000, // 3: set pins, 0 [16]
    0x0000  // 4: jmp 0
};

static const struct pio_program blink_prog = {
    .instructions = blink_instructions,
    .length = 5,
    .origin = -1,
};

void setup() {
    float div = (float)clock_get_hz(clk_sys) / (1000.0f * 100.0f);
    const uint led_pin = 15; 
    PIO pio = pio0;
    pio_clear_instruction_memory(pio);
    uint offset = pio_add_program(pio, &blink_prog);
    uint sm = pio_claim_unused_sm(pio, true);
    pio_sm_config c = pio_get_default_sm_config();
    sm_config_set_set_pins(&c, led_pin, 1);
    pio_gpio_init(pio, led_pin);
    pio_sm_set_consecutive_pindirs(pio, sm, led_pin, 1, true);
    sm_config_set_clkdiv(&c, div);
    pio_sm_init(pio, sm, offset, &c);
    pio_sm_set_enabled(pio, sm, true);
}

void loop() {
}
Но одних только команд - мало.
инструкцией
Код:
static const uint16_t blink_instructions[] = {0xff01, 0xf101, 0xff00, 0xf000, 0x0000};
мы просто создали массив blink_instructions[] с 16-битными данными.
Надо его загрузить в память PIO.

Создаём дескриптор программы blink_prog для PIO SDK, который сообщает системе, как загрузить код в память инструкций PIO.
Код:
static const struct pio_program blink_prog = {
    .instructions = blink_instructions,
    .length = 5,
    .origin = -1,
};
Что мы сообщаем:
- программа называется blink_prog,
- инструкции хранятся в массиве blink_instructions,
- длина программы = 5 инструкций (инструкции у нас 16-битные, нулевая инструкция тоже считается),
- программа релоцируемая (.origin = -1). Она может быть загружена по любому адресу в памяти PIO (от 0 до 31). Если бы здесь стояло число, например 5, программа могла бы работать только при загрузке строго в пятую ячейку памяти.

Далее, в блоке setup надо запустить наш конечный автомат.
Код:
float div = (float)clock_get_hz(clk_sys) / (1000.0f * 100.0f);
Считаем делитель
Код:
const uint led_pin = 15;
Назначаем пин, с которым будет работать конечный автомат
Код:
PIO pio = pio0;
Указываем аппаратный блок, из которого вызовем конечный автомат
В RP2040 доступны два независимых блока (pio0 и pio1), каждый блок имеет свою память инструкций по 32 инструкции на блок.
В каждом блоке можно запустить до 4-х самостоятельных конечных автомата.
Сейчас мы используем pio0.
Код:
pio_clear_instruction_memory(pio);
Очистить все инструкции в аппаратном блоке pio = pio0
Код:
uint offset = pio_add_program(pio, &blink_prog);
pio_add_program добавляет программу blink_prog в аппаратный блок pio = pio0
поскольку программа релоцируемая, то функция возвращает значение offset - фактический адрес начала размещения программы
Код:
uint sm = pio_claim_unused_sm(pio, true);
Запрашиваем свободный конечный автомат из аппаратного блока pio (который у нас pio0)
true: Этот флаг (panic) означает: «Если свободных машин нет - немедленно останови работу всей программы (вызови panic)».
функция резервирует и возвращает sm - номер свободной State Machine (от 0 до 3).
Код:
pio_sm_config c = pio_get_default_sm_config();
Создаём объект конфигурации С со значениями «по умолчанию» для нашего конечного автомата.
В PIO огромное количество настроек, лучше сначала загрузить дефолтные их значения и внести правки.
Код:
sm_config_set_set_pins(&c, led_pin, 1);
В структуре конфигурации С присвоить базовый индекс пина (led_pin=15) инструкции set pins. Работать с последовательностью пинов длиной 1 (т.е. только с одним 15 пином). Если бы было 2, то пины 15 и 16 управлялись командой set pins одновременно.
Код:
pio_gpio_init(pio, led_pin);
Выполняем аппаратную коммутацию пина (led_pin=15) от стандартного программного управления (функций вроде digitalWrite) на аппаратный блок (pio = pio0)
Код:
pio_sm_set_consecutive_pindirs(pio, sm, led_pin, 1, true);
Для конечного автомата sm в аппаратном блоке pio устанавливаем направление работы (input/output) для группы пинов начиная с (led_pin=15), длина группы - 1, направление true означает выход (Output), false означало бы вход.
Код:
sm_config_set_clkdiv(&c, div);
В структуре конфигурации С записываем рассчитанный коэффициент деления div.
Код:
pio_sm_init(pio, sm, offset, &c);
Записываем все настройки конфигурации С в конечный автомат sm аппаратного блока pio. Счётчик команд конечного автомата устанавливается на ячейку инструкций offset. Внутренние регистры конечного автомата очищаются.
Код:
pio_sm_set_enabled(pio, sm, true);
Запускается работа (true) конечного автомата sm аппаратного блока pio (команда со значением false мгновенно остановит конечный автомат).
С этого момента процессор больше не участвует в генерации меандра.

Результат - ровно 1000 Гц
Вложение:
1KHZ.jpg
1KHZ.jpg [ 138.95 КБ | 88 просмотров ]


Вложения:
1KHZ.rar [133.83 КБ]
3 скачивания
Вернуться к началу
Показать сообщения за:  Поле сортировки  
Начать новую тему  Ответить на тему  [ 44 сообщения ]  На страницу Пред. 1 2 3 4 5 След.

Часовой пояс: UTC


Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 0 гостей


Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете добавлять вложения

Найти:
Перейти:  
Создано на основе phpBB® Forum Software © phpBB Limited
Русская поддержка phpBB