Jupyter Notebook — любимый инструмент-среда для data scientist'ов, аналитиков, инженеров, математиков, студентов и даже для нас — самых обычных ученых в экспериментальной физике.


Этот инструмент предназначен для работы с интерпретируемыми языками и удобного графического представления данных. Долгое время мы просто считали на нем, используя Python и математические библиотеки (numpy, SciPy, matplot и т.д.). Но оказывается данная среда не так проста и имеет гораздо больший потенциал. Очень неожиданно, но Jupyter позволяет легко манипулировать электронными устройствами на микроконтроллерах, может служить чем-то вроде REPL среды для МК только без слабенького MicroPython и внушительной поддержкой переферии чипа, причем все это почти из коробки.



Небольшое интро.


Как то раз из братского НИИ к нам пришел измерительный прибор, хотя прибор — это громко сказано, скорее примитивный сенсор, данные с которого требуется еще обрабатывать. Алгоритм простой, всего лишь нужно измерять время нарастания фронта сигнала. Что-то типа такого:



К сожалению, данные и форму реального сигнала, привести увы, не могу.


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


Очень не хотелось сидеть и портить глаза об экран осцилографа и примитивные расчеты. А дело мог исправить какой-нибудь USB или Ethernet осцилограф, хотя бы на несколько десятков килогерц, однако который стоит десяток килогривен. С его помощью можно было получать данные сигнала на компьютер и там уже обрабатывать в Python.


Из того что было: обычно вечно выручающая ардуинка не вытягивала такой sample rate для оцифровки сигнала — только 10,000 times a second а по факту меньше, что то около 8,000 times a second. Нужно было верхнюю частоту хотя бы 40,000 -50,000 times a second.


Была даже отчайная мысль, поставить смартфон и сделать распознование сигнала с осцилографа на камеру. Но сигнал с сенсора сильно меняется по частоте(на порядок) поэтому все равно придется сидеть рядом с осцилографом и подправлять развертку на нем, что бы сигнал оптимально отображался на экране. Уже лучше, но опять же от осцилографа далеко не отойдешь, к тому же точность скорее всего будет не очень и в конце концов для нас, самыx обычных физиков, computer vision это настоящий rocket science. Увы.


Опрос по коллегам показал, что самое бюджетное решение в таком случае использовать более мощную электронную плату чем ардуино и на её основе делать измерительное устройство, и мне даже вручили набор подобного оборудования. Однако работы для такого варианта все равно было много, нужно разбираться с программированием микроконтроллеров, со средой програмирования микроконтроллера и еще нужно написать софт который будет принимать данные с устройства. Трудозатрат получается все равно очень много.


Самый дельный совет, дал мой эмигрировавший однокурсник Боря. В его европейском университете есть интересная методика для работы с микропроцессорной техникой. Там студенты изучают функционирование устройств на микроконтроллерах с помощью Jupyter Notebook. Здесь Jupyter выступает в качестве REPL среды для исполнения команд на МК, это очень похоже на принцип MicroPython. Только MicroPython запускается на микроконтроллере и исполняет код, который пользователь пишет в терминал COM-порта.



https://www.youtube.com/watch?v=5LbgyDmRu9s


А в случае с Jupyter подход другой. Jupyter Notebook запускается на обычном компьютере, а взаимодействие с МК происходит через удаленный вызов процедур библиотеки драйверов. К примеру для микроконтроллеров STM32 и STM8, такую библиотеку драйверов предоставляет производитель этих микроконтроллеров — компания STMicroelectronics. Называется библиотека Standard Peripheral Library


Посмотрим на примере: в этой библиотеки для чипа STM8 есть ф-ция для работы с АЦП.


uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);

Она возращает значение оцифрованного сигнала. Её можно очень просто вызвать в Jupyter:



Как вы могли заметить ADC_GetConversionValue имеет сигнатуру С/С++ ф-ция, как же я её вызвала из Jupyter?! Есть полезный плагин xeus-cling, который к Jupyter добавляет интерпретатор С++. Огорчает только одно, на Windows этот плагин не работает. Так что здесь, мне давней пользовательнице продукта Microsoft, пришлось воспользоваться вирутальной машиной c установленной Ubuntu. Можно воспользоваться MacOS, но откуда бы MacBook у нас взялся в лаборатории...


xeus-cling это только С/С++ интерпретатор, трансляцией вызова ф-ции из среды Jupyter NoteBook в микроконтроллер занимается библиотека REMCU. Это обычная динамическая библиотека, которая предоставляет такой же API, как у Standard Peripheral Library т.е. можно подключить заголовочные(header) файлы из Standard Peripheral Library, а вернее для STM8LDiscovery всего один основной заголовочный файл требуется для работы с периферией:


#include "stm8l15x.h"

И можно пользоваться всеми функциями из этого заголовочного файла, передавать туда необходимые параметры для работы с периферией, как это обычно делается при написании прошивки для микроконтроллера. Библиотека сама все оттранслирует в МК без написания дополнительных оберток.


По сути дела, работа с аппаратными модулями микроконтроллера в Jupyter в таком случае, ничем не отличается от работы с периферией из прошивки микроконтролера. Конечно, напрямую к регистрам обращаться нельзя, вроде такого:


/* Init GPIO Port C PC7 output push-pull up to 10MHz blue LED4*/
    GPIOC->DDR |= 0x80;     
    GPIOC->CR1 |= 0x80; 
    GPIOC->CR2 |= 0x80;

Пример WavesGenerator


Потому как Jupyter запущен на компьютере и закономерно упадет при такой операции. Но и через Standard Peripheral Library все аппартные блоки доступны для управления.


Предлагаю уже попробовать попрактиковаться. Нам понадобится:


  • Компьютер (или вирутальная машина) с Ubuntu или MacOS на ней должна быть установлена Anaconda, Jupyter Notebook и к нему плагины xeus-cling и xplots(для построения графиков)
  • Электронная плата STM8LDiscovery — по совету коллеги, как самый простой вариант среди доступного тогда оборудования. Можно использовать и другие платы, к примеру c STM32.
  • Библиотека REMCU для STM8LDiscovery, в прошлом году со мной поделился Борис. Но сейчас библиотека уже доступна для скачивания с сайта.

Как скачать

Для платы STM8LDiscovery выбрать STM8L152C6 в поле MCU name:, библиотеку StdPeriph_Driver и я использовала версию 1.4.0

И скачать архив с библиотекой.


  • OpenOCD утилита, нужна для работы с REMCU. Рекомендую скачивать из этого репозитория, потому как OpenOCD, который ставится в Ubuntu через apt install не имел поддержки платы STM8LDiscovery.
  • ST-Link stick — отдельный программатор для STM8LDiscovery, стоит меньше чем сто гривен. Хотя вроде бы STM8LDiscovery полноценная отладочная плата, но встроенный программтор не может работать с OpenOCD.
    UPD. В комментариях к статье дали способ обойтись без внешнего ST-Link

Первым делом подключаем программтор ST-Link к STM8LDiscovery и далее к компьютеру.


Схема


Запускамем OpenOCD с такими параметрами:


./openocd -f interface/stlink-v2.cfg -f target/stm8l.cfg

run openocd


Теперь можно запускать Jupyter и проводить эксперименты с STM8LDiscovery. Однако библиотека Standard Peripheral Library оказалась не такая простая как библиотека Arduino. Она имеет гораздо больше возможностей и настроек, но из-за этого ей очень сложно пользоваться новичкам, при том что документация очень скудная. Спасает только большое кол-во примеров, которые идут с ней.


Для начала я выбрала самый простой пример GPIO_Toggle — просто поморгать светодиодами на плате. Шаблон для работы c REMCU мне тогда дали, сейчас этот шаблон можно найти в GitHub репозитории с примерами REMCU. У меня получился вот такой вот notebook:


код с минимум комментариев :

STM8L-Discovery GPIO example


Load REMCU shared libray


.L libremcu.so

Add path with header files


.I remcu_include

Including necessary header files. The “remcu.h” header must be always included before any MCU header files.


#include "remcu.h"
#include "stm8l15x.h"

remcu_connect2OpenOCD(debug_server_ip, default_openocd_port, timeout_sec)

remcu_resetRemoteUnit(__HALT)

Setting up microcontroller peripherals:


GPIO_Init(GPIOE, GPIO_Pin_7, GPIO_Mode_Out_PP_High_Fast)

GPIO_Init(GPIOC, GPIO_Pin_7, GPIO_Mode_Out_PP_High_Fast)

GPIO_ResetBits(GPIOE, GPIO_Pin_7);
GPIO_ResetBits(GPIOC, GPIO_Pin_7);

Открываем notebook в Jupyter.


Первые 6 ячеек c кодом иницилизируют работу REMCU. После этих строчек можно уже вставлять код из Standard Peripheral Library и экспериментировать. Что моргунть светодиодами на плате STM8L-Discovery, нужно вызвать всего две ф-ции:


void GPIO_Init(GPIO_TypeDef* GPIOx, uint8_t GPIO_Pin, GPIO_Mode_TypeDef GPIO_Mode);
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint8_t GPIO_Pin);

Но параметры из примера что шли в SDK, мне не подходили. Пример сделан для другой платы STM8L15X_EVAL. В интеренете удалось найти параметры подходящие для моего устройства:


GPIO_Init(GPIOE, GPIO_Pin_7, GPIO_Mode_Out_PP_High_Fast);
GPIO_Init(GPIOC, GPIO_Pin_7, GPIO_Mode_Out_PP_High_Fast);

Что бы запустить пример, надо распокавать архив с REMCU библиотекой в папку с notebook файлом.



OpenOCD должен быть запущен.


Выполняем все ячейки в notebook, при выполнении ф-ции GPIO_Init светодиоды зажглись:



И когда выполняем ф-ции GPIO_ResetBits, они гаснут


Здесь все просто как в Arduino. С АЦП тоже несложно работать:


ADC code
      /* Enable ADC1 clock */
      CLK_PeripheralClockConfig(CLK_Peripheral_ADC1, ENABLE);

      /* Initialize and configure ADC1 */
      ADC_Init(ADC1, ADC_ConversionMode_Continuous, ADC_Resolution_12Bit, ADC_Prescaler_2);

      /* ADC channel used for IDD measurement */
      ADC_SamplingTimeConfig(ADC1, ADC_Group_SlowChannels, ADC_SamplingTime_384Cycles);

      /* Enable ADC1 */
      ADC_Cmd(ADC1, ENABLE);

      /* Start ADC1 Conversion using Software trigger*/
      ADC_SoftwareStartConv(ADC1);

      /* Get conversion value */
      ADCData = ADC_GetConversionValue(ADC1);

Однако удаленный вызов ф-ции в микроконтроллере STM8 далеко не быстрый процесс и достигнуть хотя бы 40,000 sample rate у меня не получится. Спасает ситуацию здесь буферизация данных, у STM8L продвинутый процессор и в нем есть полезный модуль DMA — это аппаратный блок в микроконтроллере, который позволяет буферизировать данные с АЦП(и других периферийных блоков) в память чипа. А Jupyter Notebook может читать накопленные данные из памяти МК — это как раз то что нам нужно. Можно обрабатывать полученные значения и высчитывать необходимое время нарастания сигнала.


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


Для работы с DMA я взяла пример ADC_DMA, получился такой notebook


Здесь с помощью Xplots строим график данных с АЦП. Генератор сигнала следует подключить к ножке PC7 платы STM8LDiscovery.


Запускаем и полученная картинка совсем не похожа на подаваемый сигнал(синус в нашем случае):



Я долго билась с таким непонятным поведением. А оказалось вот чем дело, у процессорного ядра есть такая х-ка как интерпретация порядка байт, а конкретно их там всего две интерпретации little-endian и big-endian. Прямой и обратный порядок байт, на всех наших офисных ПК используется little-endian, а в STM8LDiscovery, big-endian! Поэтому в данных, что приходят из АЦП поменяны старший байт с младшим. Если сделать престановку байт, то картинка будет как и предполагалось для синусоидального сигнала.


#include <netinet/in.h>

for(int i = 0; i < 0xFF; i++){
    uint16_t temp = adc_data[i];
    temp = htons(temp);
    adc_data[i] = temp;
}
line.y = adc_data; //plot graph
fig


Но можно заметить, что на графике есть артефакты.



Это из-за того что буфер в памяти МК перезаписывается значениями АЦП по кругу. В какой-то момент времени мы останавливаем запись и читаем из памяти STM8LDiscovery сохраненные значения. К сожалению, запись останавливается в произвольный момент времени, поэтому мы видим такие артефакты. Конечно было бы удобнее если бы запись останавливался бы в конце буфера, но мне не понятно как это сделать.


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



Тогда мы получим график сигнала без артефактов посередине. Делается это просто:


//временный буфер
int osc_data[ADC_BUFFER_SIZE] = {0};

//На какой ячейке остановилась запись DMA
size_t shift =  DMA_GetCurrDataCounter(DMA1_Channel0);
//сохраняем начальные данные
for(int i = 0; i < ADC_BUFFER_SIZE; i++){ osc_data[i] = adc_data[i];}
//считаем смещение, на которое надо сдвинуть сигнал что бы он стал ровный как на картинке осцилографа
shift =  ADC_BUFFER_SIZE - shift;
//сдвигаем
for(size_t i = 0; i < ADC_BUFFER_SIZE; i++){
        int shift_pos = (i + shift) % ADC_BUFFER_SIZE;
        adc_data[i] = osc_data[shift_pos];
    }
line.y = adc_data; //plot graph
fig


Можно вообще не заморачиваться с перестановкой байт в данных. А просто перевести АЦП в режим 8бит. Когда один байт — никаких проблем с его порядком нет!


Огорчил меня немного модуль DMA у STM8LDiscovery. Он позволяет держать буфер всего в 255 значений. Это очень мало для работы с нашим сигналом, у которого частота меняется на порядок. Желательно ну хотя бы 10.000 значений. Кстати, в примерах к плате собрату STM8LDiscovery — STM32F4Discovery. Для STM32F4 чипа можно задавать буфер DMA в более чем 64 тысячи значений, это было бы удобно для нашей задачи. Но в то время, пришлось иметь дело с тем что было и пришлось динамически задавать sample rate для АЦП, так что бы один период сигнала оптимально умещался в 255 значениях.


Частотой сэмплирования АЦП, управляет таймер TIM1. Настроить его можно с помощью ф-ции :


void TIM1_TimeBaseInit(uint16_t TIM1_Prescaler,
                       TIM1_CounterMode_TypeDef TIM1_CounterMode,
                       uint16_t TIM1_Period,
                       uint8_t TIM1_RepetitionCounter);

Здесь много параметров, которые по хорошему надо высчитывать по документации на чип. Но мы не физики-теоретики, а экспериментаторы. Поэтому я просто подбирала параметры экспериментируя. Для меня самое удобное оказалось это регулировать частоту опроса с помощью аргумента TIM1_Period, при остальных фиксированных аргументах. От TIM1_Period идет почти линейная зависимость частоты опроса, а от остальных нет.


В этом коде АЦП работает на максимальной скорости опроса, вернее максимально, что у меня получилось подобрать при таких параметрах:


SamplingTime = ADC_SamplingTime_4Cycles;
TIM1_Prescaler = 0x0; 
TIM1_Period = 0x2;
TIM1_RepetitionCounter = 0;

Это, по моим оценкам, примерно 600-660 кГц. Вы можете изменить скорость для своих нужд подбирая переменные выше.


На всякий случай, если у вас нет сигнального генератора для эксперимента с АЦП, я добавила в notebook, еще и работу с ЦАП(он же DAC) микроконтроллера STM8LDiscovery. ЦАП, опять же, с помощью DMA генерирует на ножку PF0 синусоидальный сигнал, который, кстати, наблюдаем на картинках выше, достаточно просто ножки PC7(вход АЦП) и PF0(выход ЦАП) соединить проводом. Выполните эти ячейки в notebook, что бы ЦАП начал работу:


Код

DAC-DMA example


 const uint16_t MEM_ADDRESS = ADC_BUFFER_SIZE*sizeof(adc_data.front()) + 1;
 const uint8_t MEM_SIZE = 130;
 uint8_t SINUS_TABLE[130] = {110,115,121,126,131,137,142,147,
152,157,161,166,171,175,179,183,187,191,195,198,201,204,207,209,
211,213,215,216,218,219,219,220,220,220,220,219,218,217,216,214,
212,210,208,205,202,199,196,193,189,185,181,177,173,168,164,159,
154,149,144,139,134,129,123,118,113,107,102,97,91,86,81,76,
71,66,61,56,52,47,43,39,35,31,27,24,21,18,15,12,
10,8,6,4,3,2,1,0,0,0,0,1,1,2,4,5,7,9,11,13,16,19,22,25,29,33,37,41,45,49,54,59,
63,68,73,78,83,89,94,99,105,110, };

  CLK_PeripheralClockConfig(CLK_Peripheral_DAC, ENABLE);
  CLK_PeripheralClockConfig(CLK_Peripheral_TIM4, ENABLE);

for(int i = 0 ; i < 130*sizeof(SINUS_TABLE[0]); i+=10){
    remcu_store2mem(MEM_ADDRESS + i, (uint8_t*)SINUS_TABLE + i, 10);
}

  /* DMA channel3 Config -----------------------------------------------------*/
#define DAC_CH1RDHRH_ADDRESS      0x5388
#define DAC_CH1RD8_ADDRESS      0x5390
#define DAC_CH1RDHLH_ADDRESS      0x538C

  DMA_DeInit(DMA1_Channel3);
  DMA_Init(DMA1_Channel3,  MEM_ADDRESS,
           DAC_CH1RD8_ADDRESS,
           MEM_SIZE, DMA_DIR_MemoryToPeripheral, DMA_Mode_Circular,
           DMA_MemoryIncMode_Inc, DMA_Priority_High,
           DMA_MemoryDataSize_Byte
          );

  /* DMA1 Channel 3 enable */
  DMA_Cmd(DMA1_Channel3, ENABLE);
  DMA_GlobalCmd(ENABLE);

  /* DAC Channel1 Config: 12bit right ----------------------------------------*/
  /* DAC deinitialize */
  DAC_DeInit();

  /* Fill DAC Init param DAC_Trigger_T4_TRGO and  DAC Channel1 Init */
  DAC_Init(DAC_Channel_1, DAC_Trigger_T4_TRGO, DAC_OutputBuffer_Enable);

  /* Enable DAC Channel1 */
  DAC_Cmd(DAC_Channel_1, ENABLE);

  /* Enable DMA for DAC Channel1 */
  DAC_DMACmd(DAC_Channel_1, ENABLE);

  TIM4_DeInit();
  /* Time base configuration */
  TIM4_TimeBaseInit(TIM4_Prescaler_1, 0x1);

  /* TIM4 TRGO selection */
  TIM4_SelectOutputTrigger(TIM4_TRGOSource_Update);

  /* TIM4 enable counter */
  TIM4_Cmd(ENABLE);

Код для работы с ЦАП взят из этого примера. Здесь я не стала заморачиваться с переводом данных в big-endian, а просто перевела ЦАП в режим 8-bit.


Замечу, что некоторые примеры из Standard Peripheral Library у меня не завелись. К примеру, код из DAC_Noise&TriangleGenerator у меня выдавал ошибку, что не найдена следующая ф-ция:


DAC_SetTriangleWaveAmplitude

Такой ф-ции нет в Standard Peripheral Library версии 1.4.0. Поэтому имейте это ввиду если будете запускать другие примеры, возможно их придется адаптировать под версию 1.4.0 либо использовть более новую версию REMCU.


Еще один ноутбук я подготовила для работы с LCD экраном на плате, его работу вы можете наблюдать на первом gif-изображении. ЖК экран не требуется нам для автоматизации физических опытов, с ним поиграться я решила скорее из любопытства. Сделать его было очень тривиально, буквально за 15 минут он заработал. Я просто скопировала код из примера к плате STM8LDiscovery для работы с LCD экраном. Немного подправила и добавила всего одну ф-цию:


void LCD_print(std::string str);

С помощью которой можно печатать текст на ЖК экране.


Мои подготовленные notebook'и можно найти на Github.


Вот такие были мои эксперименты с микроконтроллерои STM8L151 в Jupyter Notebok. Это была моя первая практика работы с чем-то кроме ардуино, с чем то более профессиональным чем ардуино и более низкоуровненым. Все прошло довольно легко, кроме ситуации с big-endian. На поиск причины в столь странных данных с АЦП у меня ушло больше всего времени и если мне не помогли, я, вероятно, не смогла бы разобраться в столь заковыристой особенности микропроцессора.


Полученный опыт считаю успешным, мы и дальше планируем применять STM8LDiscovery и более профессиональную модель STM32F4Discovery для автоматизации экспериментов. А сейчас даже уже появились байдинги библиотеки REMCU для Python. Поэтому можно уже обойтись без виртуальной машины с Linux или MacOS, а писать скрипты в родной оконной системе.


Всем интересных опытов,
Да прибудет с вами Jupyter!