В данном ноутбуке вам предлагается написать различные обработчики для команд с компьютера.Также можно реализовать свой дополнительный набор команд под свои задачи.
Сперва подключите все библиотеки уставленные в подготовительной статье
In [ ]:
import serial
import pyaudio
import numpy as np
import wave
import scipy.signal as signal
import warnings
warnings.filterwarnings('ignore')
Здесь уже заготовлены стандартные функции для работы с 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()
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