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

Подключение всех библиотек

Сперва подключите все библиотеки уставленные в подготовительной статье


In [ ]:
import serial
import pyaudio
import numpy as np
import wave
import scipy.signal as signal
import warnings
warnings.filterwarnings('ignore')

Стандартные функции для работы с UART

Здесь уже заготовлены стандартные функции для работы с UART. Функции даны лишь для удобства и их использование обязательным не является.


In [ ]:
def serial_init(speed):
    dev = serial.Serial(
        # Здесь указывается устройство, с которым будет производится работа
        # /dev/ttyUSBx - для Linux
        # /dev/tty.SLAB_USBtoUART - для MacOS
        port='/dev/ttyUSB0', 
        # Скорость передачи
        baudrate=speed,
        # Использование бита четности
        parity=serial.PARITY_NONE,
        # Длина стоп-бита
        stopbits=serial.STOPBITS_ONE,
        # Длина пакета
        bytesize=serial.EIGHTBITS,
        # Максимальное время ожидания устройства
        timeout=0.1
    )
    return dev

def serial_recv(dev):
    # Для простоты макс. кол-во символов для чтения - 255. Время ожидания - 0.1
    # decode необходим для конвертирования набора полученных байтов в строку
    string = dev.read(255).decode()
    return string

def serial_send(dev, string):
    # encode конвертирует строку в кодировке utf-8 в набор байтов 
    dev.write(string.encode('utf-8'))

Проверка работоспособности

Для проверки работоспособности замкните выводы RXD и TXD у конвертера, после этого загрузите написанную вами программу с повторителем пакетов и запустите клетку еще раз. Если возникает ошибка, то скорее всего не хватает прав (на MacOS такой проблемы быть не должно):

sudo adduser YOUR_USER_NAME dialout
sudo chmod a+rw /dev/ttyUSB0

In [ ]:
dev = serial_init(115200)
serial_send(dev, "Hello, world!")
ans = serial_recv(dev)
print(ans)

Исполнительное устройство

В этой части вам предлагается написать первую команду - управление светодиодом. Но прежде чем приступить к имплементации, необходимо определиться с форматом передачи данных между устройствами. Для простоты предлагается использовать два блока: первый хранит номер команды, второй - необходимые аргументы. Для этого объявите следующую структуру в main.c:

typedef struct {
    // Номер команды
    uint8_t cmd;
    // Необходимые параметры
    uint8_t params[10];
    // Флаг о том, что была принята новая команда
    uint8_t active;
} uart_req_t;

После этого объявите статическую глобальную переменную данного типа:

static uart_req_t uart_req;

Теперь придется немного модифицировать обработчик для USART1: после начала приема пакетов необходимо первый байт записать в поле cmd структуры uart_req, а все остальные байты в params до тех пор, пока не будет выставлен флаг IDLE:

void USART1_IRQHandler(void)
{
    static uint8_t pos = 0;

    if (LL_USART_IsActiveFlag_RXNE(USART1)) {
        /*
         * Если pos равен 0, то байт нужно положить в cmd, 
         * иначе в params
         * Не забудьте увеличить значение pos
         */
    }
    if (LL_USART_IsActiveFlag_IDLE(USART1)) {
        /*
         * Если был выстален флаг IDLE, то прием завершился,
         * необходимо сбросить pos и выставить флаг active
         */
        LL_USART_ClearFlag_IDLE(USART1);
    }
    return;
}

Пришло время написать сам менеджер запросов:

static void manage_requests(void) {
    /*
     * Этой переменной каждый обработчик присваивает статус после
     * завершения работы: 1 - ошибка, 0 - нет ошибок
     */
    uint8_t is_ok = 0;

    /* 
     * Если нет активных запросов - на выход
     */
    if (!uart_req.active)
        return;

    /* 
     * Здесь будут все обработчики, каждый со своим кодом
     */
    switch (uart_req.cmd) {
    default:
        is_ok = 1;
        break;
    }

    /*
     * Здесь отправляется ответ
     * 0x30 необходимо, чтобы привести цифру к символу
     */
    while (!LL_USART_IsActiveFlag_TXE(USART1));
    LL_USART_TransmitData8(USART1, is_ok + 0x30);

    /*
     * Сброс флага запроса
     */
    uart_req.active = 0;
    return;
}

Теперь добавьте его вызов в бесконечный цикл в main.

Первый обработчик - управление светодиодами

После написания менеджера напишите обработчик, который будет управлять светодиодом. Пусть символ 8 будет кодом команды для включения/выключения восьмого светодиода на порту GPIOC. Если передан символ 0 в качестве аргумента, то необходимо выключить светодиод, если 1, то включить.

// Этот case нужно добавить в менеджер запросов
    case '8': {
        if (uart_req.params[1] == '1')
            LL_GPIO_SetOutputPin(GPIOC, LL_GPIO_PIN_8);
        else
            LL_GPIO_ResetOutputPin(GPIOC, LL_GPIO_PIN_8);
        is_ok = 1;
        break;
    }

Загрузите прошивку и попробуйте следующей командой с компьютера зажечь синий светодиод:


In [ ]:
serial_send(dev, "8 1")

Напишите такой же обработчик для зеленого светодиода и зажгите его:


In [ ]:
serial_send(dev, "9 1")

Попробуйте потушить одновременно два светодиода


In [ ]:
serial_send(dev, "8 0")
serial_send(dev, "9 0")

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


In [ ]:
serial_send(dev, "8 1")
serial_recv(dev)
serial_send(dev, "9 0")

Детектирование бита в музыкальном треке

Данный пункт является необязательным

Цель данного примера это показать, как можно использовать компьютер и МК для решения общей задачи. Задача состоит в том, чтобы детектировать ритм музыки. Производительности микроконтроллера не достаточно для обработки звука и тяжелых расчетов, но МК вполне сможет помигать светодиодом в нужные моменты. При желании данный пример можно улучшить, подключив линейнуй шкалу к МК. С 3 индикаторами можно уже сделать простой спектроанализатор. Вот так МК может работать в качестве исполнительного устройства.

Запустите код ниже. Идея состоит в том, чтобы захватывать звуковое окно каждые 1024 семпла и рассчитывать точки, в которых происходит резкое увеличение энергии в низких частотах. Для подробного изучения более простой версии алгоритма, можно обратиться к данной статье.


In [ ]:
class AudioFile:
    chunk = 1024

    def __init__(self, file):
        """ Init audio stream """ 
        self.wf = wave.open(file, 'rb')
        self.p = pyaudio.PyAudio()
        self.stream = self.p.open(
            format = self.p.get_format_from_width(self.wf.getsampwidth()),
            channels = self.wf.getnchannels(),
            rate = self.wf.getframerate(),
            output = True
        )
        self.beatframe = np.empty(0)

    def play(self, dev, max_samples):            
        block_cnt = 0        
        B, A = signal.butter(N=3, Wn=0.9, output='ba')
        self.beatframe = np.empty(0)
        self.peak = np.zeros(max_samples)
        data = self.wf.readframes(self.chunk)
        led_lock = 10
        
        while data != '' and block_cnt != max_samples:
            block_cnt += 1
                
            self.stream.write(data)
            data = self.wf.readframes(self.chunk)
            sample = np.frombuffer(data, dtype=np.int16)
            
            # Extracting low band
            fft = np.abs(np.fft.rfft(sample))
            flg_diff = (fft[:30]**2).mean()/float(0xFFFFFFFF)
            
            # Filtering
            self.beatframe = np.append(self.beatframe, flg_diff)   
            fft_final = np.diff(self.beatframe)
            if (block_cnt <= 13):
                continue
            fft_final = signal.filtfilt(B, A, fft_final)                
            fft_final = np.where(fft_final < 0, 0, fft_final)
            
            # Detecting peaks     
            fft_range_window = np.max(fft_final[-5:])/np.max(fft_final[-25:])
            
            if (fft_range_window >= 0.90 and led_lock >= 10):
                serial_send(dev, "8 1")
                led_lock = 0
            else:
                serial_send(dev, "8 0")
            
            led_lock += 1
            
        return fft_final

    def close(self):
        """ Graceful shutdown """ 
        self.stream.close()
        self.p.terminate()

Используйте файл music.wav, который лежит в папке с ноутбуком. Можно запустить любой другой, но он должен быть моно и в wav формате. 400 отсчетов по 1024 семпла хватит примерно на 10 сек проигрывания.


In [ ]:
dev = serial_init(115200)
a = AudioFile("music.wav")
fft = a.play(dev, 400)
a.close()

Можно посмотреть на отфильтрованный сигнал. Резкие скачки амплитудой >600 это и есть моменты, где резко меняется энергия в мелодии (drum kick)


In [ ]:
plt.figure(figsize=(20,7))
plt.plot(fft, label='filtered low pass')
plt.axis('tight')
plt.legend()
plt.show()

Проверка нажатия кнопки

Теперь напишите обработчик, который будет возвращать состояние кнопки. Пусть код этого запроса будет 0.

Обработчик в manage_requests должен получиться в одну строчку


In [ ]:
serial_send(dev, '0')
state = serial_recv(dev)
if (state == '0'):
    print("Button is not pressed:(")
else:
    print("Button is pressed:)")

Семисегментный индикатор

Теперь напишите обработчик, который будет выводить на семисегментный дисплей число, переданное в качестве аргумента. Пусть код данной команды будет 1. Далее сделайте счетчик, увеличающий значение каждую секунду. Используйте sleep для формирования задержки. Документация тут.


In [ ]:
# your_code

Энкодер

Напишите обработчик для чтения текущего угла поворота энкодера.

Задание со звездочкой: на основании этих данных попробуйте посчитать скорость вращения $\omega$ и угловое ускорение $\varepsilon$. Постройте графики


In [ ]:
# your_code

ШИМ

Напишите команду, с помощью которой можно менять яркость светодиодов

Задание со звездочкой: используя код из примера с детектированием ритма, напишите программу, которая будет менять яркость светодиода в зависимости от интенсивности звукового сигнала


In [ ]:
# your_code