Quellcode durchsuchen

Add oscilloscope from https://github.com/xMasterX/all-the-plugins

git-subtree-dir: oscilloscope
git-subtree-mainline: aee7b26c37dd6ab2098432ce37a707a796087096
git-subtree-split: a7c091eb13bf1a2853fd50a3c7b9f5d2cd4c9213
Willy-JL vor 1 Jahr
Ursprung
Commit
8f46a61ae2

+ 1 - 0
oscilloscope/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/flipperscope

+ 73 - 0
oscilloscope/README.md

@@ -0,0 +1,73 @@
+# flipperscope
+
+To compile and install
+
+```
+cd flipperzero-firmware/applications_user
+git clone https://github.com/anfractuosity/flipperscope.git
+cd ..
+./fbt DEBUG=1 VERBOSE=1 fap_scope
+./fbt launch_app APPSRC=applications_user/flipperscope
+```
+
+Alternatively the binary can now be installed from https://lab.flipper.net/apps/flipperscope or the Flipper Mobile App.
+
+Provide signal to **pin 16/PC0**, with a voltage ranging from 0V to 2.5V and ground to **pin 18/GND**.
+
+Press the 'ok' button (button in the centre of joypad) to pause/unpause the waveform display.
+
+[Demo](https://www.youtube.com/watch?v=tu2X1WwADF4) showing three different waveform types from a signal generator.
+
+Also see [Derek Jamison's demonstration](https://www.youtube.com/watch?v=iC5fBGwCPHw&t=374s) of this app as well as other interesting projects.
+
+## Measurements
+
+* Measures frequency of waveform in hertz
+* Measures voltage: min, max, Vpp
+
+![Signal Generator](photos/sig.jpg)
+
+![Flipper Zero running flipperscope](photos/freq.jpg)
+
+![Rigol](photos/rigol.jpg)
+
+![Flipper Zero running flipperscope](photos/volt.jpg)
+
+## Processing captures
+
+You can use the following simple Python script, for processing the captured waveforms, from flipperscope.
+
+```
+import matplotlib.pyplot as plt
+import numpy as np
+import struct
+
+fig, ax = plt.subplots()
+data = open("Sine.dat", "rb").read()
+y = [(float(x[0]) / 2500) * 2.5 for x in struct.iter_unpack("<H", data)]
+x = np.arange(len(y))
+
+ax.plot(x, y, label='Voltage')
+ax.set_xlabel('Sample')
+ax.set_ylabel('Voltage / V')
+ax.set_title('ADC Voltage')
+ax.set_ylim([0, 2.5])
+ax.legend()
+plt.show()
+```
+
+![Captured waveform](photos/sine.png)
+
+## To Do
+
+* Customisable input pin
+* Trigger type mode
+* FFT
+* ...
+
+## Inspiration
+
+* For GUI menu system, used code principles from weather station and signal generator apps
+* STM32 DMA example
+* VREFBUF information - https://community.st.com/s/question/0D53W00001awIlMSAU/enable-and-use-vrefbuf-for-adc-measures
+* Relocating vector table - https://community.nxp.com/t5/i-MX-Processors/Relocate-vector-table-to-ITCM/m-p/1302304

+ 14 - 0
oscilloscope/application.fam

@@ -0,0 +1,14 @@
+App(
+    appid="flipperscope",
+    name="Scope",
+    fap_author="anfractuosity",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="scope_main",
+    requires=["gui"],
+    stack_size=1 * 1024,
+    fap_category="GPIO",
+    fap_icon="scope_10px.png",
+    fap_icon_assets="icons",
+    fap_version="0.2",
+    fap_description="Oscilloscope application - apply signal to pin 16/PC0, with a voltage ranging from 0V to 2.5V and ground to pin 18/GND"
+)

BIN
oscilloscope/icons/pause_10x10.png


BIN
oscilloscope/icons/play_10x10.png


+ 368 - 0
oscilloscope/scenes/adc.c

@@ -0,0 +1,368 @@
+#include "stm32wbxx_ll_adc.h"
+#include <furi.h>
+#define assert_param furi_assert
+
+/* Check of parameters for configuration of ADC hierarchical scope:           */
+/* common to several ADC instances.                                           */
+#if defined(ADC_SUPPORT_2_5_MSPS)
+#define IS_LL_ADC_COMMON_CLOCK(__CLOCK__)                                                      \
+    (((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV1) || ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV2) ||   \
+     ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV4) || ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV6) ||   \
+     ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV8) || ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV10) ||  \
+     ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV12) || ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV16) || \
+     ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV32) || ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV64) || \
+     ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV128) || ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV256))
+#else
+#define IS_LL_ADC_COMMON_CLOCK(__CLOCK__)                                                        \
+    (((__CLOCK__) == LL_ADC_CLOCK_SYNC_PCLK_DIV1) ||                                             \
+     ((__CLOCK__) == LL_ADC_CLOCK_SYNC_PCLK_DIV2) ||                                             \
+     ((__CLOCK__) == LL_ADC_CLOCK_SYNC_PCLK_DIV4) || ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV1) || \
+     ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV2) || ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV4) ||     \
+     ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV6) || ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV8) ||     \
+     ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV10) || ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV12) ||   \
+     ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV16) || ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV32) ||   \
+     ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV64) || ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV128) ||  \
+     ((__CLOCK__) == LL_ADC_CLOCK_ASYNC_DIV256))
+#endif /* ADC_SUPPORT_2_5_MSPS */
+
+/* Check of parameters for configuration of ADC hierarchical scope:           */
+/* ADC instance.                                                              */
+#if defined(ADC_SUPPORT_2_5_MSPS)
+#define IS_LL_ADC_CLOCK(__CLOCK__)                   \
+    (((__CLOCK__) == LL_ADC_CLOCK_SYNC_PCLK_DIV4) || \
+     ((__CLOCK__) == LL_ADC_CLOCK_SYNC_PCLK_DIV2) || \
+     ((__CLOCK__) == LL_ADC_CLOCK_SYNC_PCLK_DIV1) || ((__CLOCK__) == LL_ADC_CLOCK_ASYNC))
+
+#endif /* ADC_SUPPORT_2_5_MSPS */
+#define IS_LL_ADC_RESOLUTION(__RESOLUTION__)                                                      \
+    (((__RESOLUTION__) == LL_ADC_RESOLUTION_12B) ||                                               \
+     ((__RESOLUTION__) == LL_ADC_RESOLUTION_10B) || ((__RESOLUTION__) == LL_ADC_RESOLUTION_8B) || \
+     ((__RESOLUTION__) == LL_ADC_RESOLUTION_6B))
+
+#define IS_LL_ADC_DATA_ALIGN(__DATA_ALIGN__) \
+    (((__DATA_ALIGN__) == LL_ADC_DATA_ALIGN_RIGHT) || ((__DATA_ALIGN__) == LL_ADC_DATA_ALIGN_LEFT))
+
+#if defined(ADC_SUPPORT_2_5_MSPS)
+#define IS_LL_ADC_LOW_POWER(__LOW_POWER__)                                                  \
+    (((__LOW_POWER__) == LL_ADC_LP_MODE_NONE) || ((__LOW_POWER__) == LL_ADC_LP_AUTOWAIT) || \
+     ((__LOW_POWER__) == LL_ADC_LP_AUTOPOWEROFF) ||                                         \
+     ((__LOW_POWER__) == LL_ADC_LP_AUTOWAIT_AUTOPOWEROFF))
+#else
+#define IS_LL_ADC_LOW_POWER(__LOW_POWER__) \
+    (((__LOW_POWER__) == LL_ADC_LP_MODE_NONE) || ((__LOW_POWER__) == LL_ADC_LP_AUTOWAIT))
+#endif /* ADC_SUPPORT_2_5_MSPS */
+/* Check of parameters for configuration of ADC hierarchical scope:           */
+/* ADC group regular                                                          */
+#if defined(ADC_SUPPORT_2_5_MSPS)
+#define IS_LL_ADC_REG_TRIG_SOURCE(__REG_TRIG_SOURCE__)            \
+    (((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_SOFTWARE) ||       \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_TIM1_TRGO2) || \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_TIM1_CH4) ||   \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_TIM2_TRGO) ||  \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_TIM2_CH4) ||   \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_TIM2_CH3) ||   \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_EXTI_LINE11))
+#else
+#define IS_LL_ADC_REG_TRIG_SOURCE(__REG_TRIG_SOURCE__)            \
+    (((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_SOFTWARE) ||       \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_TIM1_TRGO) ||  \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_TIM1_TRGO2) || \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_TIM1_CH1) ||   \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_TIM1_CH2) ||   \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_TIM1_CH3) ||   \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_TIM2_TRGO) ||  \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_TIM2_CH2) ||   \
+     ((__REG_TRIG_SOURCE__) == LL_ADC_REG_TRIG_EXT_EXTI_LINE11))
+#endif /* ADC_SUPPORT_2_5_MSPS */
+
+#define IS_LL_ADC_REG_CONTINUOUS_MODE(__REG_CONTINUOUS_MODE__) \
+    (((__REG_CONTINUOUS_MODE__) == LL_ADC_REG_CONV_SINGLE) ||  \
+     ((__REG_CONTINUOUS_MODE__) == LL_ADC_REG_CONV_CONTINUOUS))
+
+#define IS_LL_ADC_REG_DMA_TRANSFER(__REG_DMA_TRANSFER__)            \
+    (((__REG_DMA_TRANSFER__) == LL_ADC_REG_DMA_TRANSFER_NONE) ||    \
+     ((__REG_DMA_TRANSFER__) == LL_ADC_REG_DMA_TRANSFER_LIMITED) || \
+     ((__REG_DMA_TRANSFER__) == LL_ADC_REG_DMA_TRANSFER_UNLIMITED))
+
+#define IS_LL_ADC_REG_OVR_DATA_BEHAVIOR(__REG_OVR_DATA_BEHAVIOR__)     \
+    (((__REG_OVR_DATA_BEHAVIOR__) == LL_ADC_REG_OVR_DATA_PRESERVED) || \
+     ((__REG_OVR_DATA_BEHAVIOR__) == LL_ADC_REG_OVR_DATA_OVERWRITTEN))
+
+#if defined(ADC_SUPPORT_2_5_MSPS)
+#define IS_LL_ADC_REG_SEQ_MODE(__REG_SEQ_MODE__)     \
+    (((__REG_SEQ_MODE__) == LL_ADC_REG_SEQ_FIXED) || \
+     ((__REG_SEQ_MODE__) == LL_ADC_REG_SEQ_CONFIGURABLE))
+#endif /* ADC_SUPPORT_2_5_MSPS */
+
+#if defined(ADC_SUPPORT_2_5_MSPS)
+#define IS_LL_ADC_REG_SEQ_SCAN_LENGTH(__REG_SEQ_SCAN_LENGTH__)           \
+    (((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_DISABLE) ||       \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_2RANKS) || \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_3RANKS) || \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_4RANKS) || \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_5RANKS) || \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_6RANKS) || \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_7RANKS) || \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_8RANKS))
+#else
+#define IS_LL_ADC_REG_SEQ_SCAN_LENGTH(__REG_SEQ_SCAN_LENGTH__)            \
+    (((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_DISABLE) ||        \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_2RANKS) ||  \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_3RANKS) ||  \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_4RANKS) ||  \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_5RANKS) ||  \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_6RANKS) ||  \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_7RANKS) ||  \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_8RANKS) ||  \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_9RANKS) ||  \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_10RANKS) || \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_11RANKS) || \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_12RANKS) || \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_13RANKS) || \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_14RANKS) || \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_15RANKS) || \
+     ((__REG_SEQ_SCAN_LENGTH__) == LL_ADC_REG_SEQ_SCAN_ENABLE_16RANKS))
+#endif /* ADC_SUPPORT_2_5_MSPS */
+#if defined(ADC_SUPPORT_2_5_MSPS)
+#define IS_LL_ADC_REG_SEQ_SCAN_DISCONT_MODE(__REG_SEQ_DISCONT_MODE__)  \
+    (((__REG_SEQ_DISCONT_MODE__) == LL_ADC_REG_SEQ_DISCONT_DISABLE) || \
+     ((__REG_SEQ_DISCONT_MODE__) == LL_ADC_REG_SEQ_DISCONT_1RANK))
+#else
+#define IS_LL_ADC_REG_SEQ_SCAN_DISCONT_MODE(__REG_SEQ_DISCONT_MODE__)  \
+    (((__REG_SEQ_DISCONT_MODE__) == LL_ADC_REG_SEQ_DISCONT_DISABLE) || \
+     ((__REG_SEQ_DISCONT_MODE__) == LL_ADC_REG_SEQ_DISCONT_1RANK) ||   \
+     ((__REG_SEQ_DISCONT_MODE__) == LL_ADC_REG_SEQ_DISCONT_2RANKS) ||  \
+     ((__REG_SEQ_DISCONT_MODE__) == LL_ADC_REG_SEQ_DISCONT_3RANKS) ||  \
+     ((__REG_SEQ_DISCONT_MODE__) == LL_ADC_REG_SEQ_DISCONT_4RANKS) ||  \
+     ((__REG_SEQ_DISCONT_MODE__) == LL_ADC_REG_SEQ_DISCONT_5RANKS) ||  \
+     ((__REG_SEQ_DISCONT_MODE__) == LL_ADC_REG_SEQ_DISCONT_6RANKS) ||  \
+     ((__REG_SEQ_DISCONT_MODE__) == LL_ADC_REG_SEQ_DISCONT_7RANKS) ||  \
+     ((__REG_SEQ_DISCONT_MODE__) == LL_ADC_REG_SEQ_DISCONT_8RANKS))
+#endif /* ADC_SUPPORT_2_5_MSPS */
+/* Check of parameters for configuration of ADC hierarchical scope:           */
+/* ADC group injected                                                         */
+#define IS_LL_ADC_INJ_TRIG_SOURCE(__INJ_TRIG_SOURCE__)            \
+    (((__INJ_TRIG_SOURCE__) == LL_ADC_INJ_TRIG_SOFTWARE) ||       \
+     ((__INJ_TRIG_SOURCE__) == LL_ADC_INJ_TRIG_EXT_TIM1_TRGO) ||  \
+     ((__INJ_TRIG_SOURCE__) == LL_ADC_INJ_TRIG_EXT_TIM1_TRGO2) || \
+     ((__INJ_TRIG_SOURCE__) == LL_ADC_INJ_TRIG_EXT_TIM1_CH4) ||   \
+     ((__INJ_TRIG_SOURCE__) == LL_ADC_INJ_TRIG_EXT_TIM2_TRGO) ||  \
+     ((__INJ_TRIG_SOURCE__) == LL_ADC_INJ_TRIG_EXT_TIM2_CH1) ||   \
+     ((__INJ_TRIG_SOURCE__) == LL_ADC_INJ_TRIG_EXT_EXTI_LINE15))
+
+#define IS_LL_ADC_INJ_TRIG_EXT_EDGE(__INJ_TRIG_EXT_EDGE__)       \
+    (((__INJ_TRIG_EXT_EDGE__) == LL_ADC_INJ_TRIG_EXT_RISING) ||  \
+     ((__INJ_TRIG_EXT_EDGE__) == LL_ADC_INJ_TRIG_EXT_FALLING) || \
+     ((__INJ_TRIG_EXT_EDGE__) == LL_ADC_INJ_TRIG_EXT_RISINGFALLING))
+
+#define IS_LL_ADC_INJ_TRIG_AUTO(__INJ_TRIG_AUTO__)           \
+    (((__INJ_TRIG_AUTO__) == LL_ADC_INJ_TRIG_INDEPENDENT) || \
+     ((__INJ_TRIG_AUTO__) == LL_ADC_INJ_TRIG_FROM_GRP_REGULAR))
+
+#define IS_LL_ADC_INJ_SEQ_SCAN_LENGTH(__INJ_SEQ_SCAN_LENGTH__)           \
+    (((__INJ_SEQ_SCAN_LENGTH__) == LL_ADC_INJ_SEQ_SCAN_DISABLE) ||       \
+     ((__INJ_SEQ_SCAN_LENGTH__) == LL_ADC_INJ_SEQ_SCAN_ENABLE_2RANKS) || \
+     ((__INJ_SEQ_SCAN_LENGTH__) == LL_ADC_INJ_SEQ_SCAN_ENABLE_3RANKS) || \
+     ((__INJ_SEQ_SCAN_LENGTH__) == LL_ADC_INJ_SEQ_SCAN_ENABLE_4RANKS))
+
+#define IS_LL_ADC_INJ_SEQ_SCAN_DISCONT_MODE(__INJ_SEQ_DISCONT_MODE__)  \
+    (((__INJ_SEQ_DISCONT_MODE__) == LL_ADC_INJ_SEQ_DISCONT_DISABLE) || \
+     ((__INJ_SEQ_DISCONT_MODE__) == LL_ADC_INJ_SEQ_DISCONT_1RANK))
+
+ErrorStatus LL_ADC_Init(ADC_TypeDef* ADCx, const LL_ADC_InitTypeDef* ADC_InitStruct) {
+    ErrorStatus status = SUCCESS;
+
+    /* Check the parameters */
+    assert_param(IS_ADC_ALL_INSTANCE(ADCx));
+
+#if defined(ADC_SUPPORT_2_5_MSPS)
+    assert_param(IS_LL_ADC_CLOCK(ADC_InitStruct->Clock));
+#endif /* ADC_SUPPORT_2_5_MSPS */
+    assert_param(IS_LL_ADC_RESOLUTION(ADC_InitStruct->Resolution));
+    assert_param(IS_LL_ADC_DATA_ALIGN(ADC_InitStruct->DataAlignment));
+    assert_param(IS_LL_ADC_LOW_POWER(ADC_InitStruct->LowPowerMode));
+
+    /* Note: Hardware constraint (refer to description of this function):       */
+    /*       ADC instance must be disabled.                                     */
+    if(LL_ADC_IsEnabled(ADCx) == 0UL) {
+        /* Configuration of ADC hierarchical scope:                               */
+        /*  - ADC instance                                                        */
+        /*    - Set ADC data resolution                                           */
+        /*    - Set ADC conversion data alignment                                 */
+        /*    - Set ADC low power mode                                            */
+#if defined(ADC_SUPPORT_2_5_MSPS)
+        MODIFY_REG(
+            ADCx->CFGR1,
+            ADC_CFGR1_RES | ADC_CFGR1_ALIGN | ADC_CFGR1_WAIT | ADC_CFGR1_AUTOFF,
+            ADC_InitStruct->Resolution | ADC_InitStruct->DataAlignment |
+                ADC_InitStruct->LowPowerMode);
+
+        MODIFY_REG(ADCx->CFGR2, ADC_CFGR2_CKMODE, ADC_InitStruct->Clock);
+#else
+        MODIFY_REG(
+            ADCx->CFGR,
+            ADC_CFGR_RES | ADC_CFGR_ALIGN | ADC_CFGR_AUTDLY,
+            ADC_InitStruct->Resolution | ADC_InitStruct->DataAlignment |
+                ADC_InitStruct->LowPowerMode);
+#endif /* ADC_SUPPORT_2_5_MSPS */
+    } else {
+        /* Initialization error: ADC instance is not disabled. */
+        status = ERROR;
+    }
+    return status;
+}
+
+ErrorStatus LL_ADC_REG_Init(ADC_TypeDef* ADCx, const LL_ADC_REG_InitTypeDef* ADC_REG_InitStruct) {
+    ErrorStatus status = SUCCESS;
+
+    /* Check the parameters */
+    assert_param(IS_ADC_ALL_INSTANCE(ADCx));
+    assert_param(IS_LL_ADC_REG_TRIG_SOURCE(ADC_REG_InitStruct->TriggerSource));
+#if defined(ADC_SUPPORT_2_5_MSPS)
+    if(LL_ADC_REG_GetSequencerConfigurable(ADCx) != LL_ADC_REG_SEQ_FIXED) {
+        assert_param(IS_LL_ADC_REG_SEQ_SCAN_LENGTH(ADC_REG_InitStruct->SequencerLength));
+    }
+    if((LL_ADC_REG_GetSequencerConfigurable(ADCx) == LL_ADC_REG_SEQ_FIXED) ||
+       (ADC_REG_InitStruct->SequencerLength != LL_ADC_REG_SEQ_SCAN_DISABLE)) {
+        assert_param(IS_LL_ADC_REG_SEQ_SCAN_DISCONT_MODE(ADC_REG_InitStruct->SequencerDiscont));
+
+        /* ADC group regular continuous mode and discontinuous mode                 */
+        /* can not be enabled simultenaeously                                       */
+        assert_param(
+            (ADC_REG_InitStruct->ContinuousMode == LL_ADC_REG_CONV_SINGLE) ||
+            (ADC_REG_InitStruct->SequencerDiscont == LL_ADC_REG_SEQ_DISCONT_DISABLE));
+    }
+#else
+    assert_param(IS_LL_ADC_REG_SEQ_SCAN_LENGTH(ADC_REG_InitStruct->SequencerLength));
+    if(ADC_REG_InitStruct->SequencerLength != LL_ADC_REG_SEQ_SCAN_DISABLE) {
+        assert_param(IS_LL_ADC_REG_SEQ_SCAN_DISCONT_MODE(ADC_REG_InitStruct->SequencerDiscont));
+
+        /* ADC group regular continuous mode and discontinuous mode                 */
+        /* can not be enabled simultenaeously                                       */
+        assert_param(
+            (ADC_REG_InitStruct->ContinuousMode == LL_ADC_REG_CONV_SINGLE) ||
+            (ADC_REG_InitStruct->SequencerDiscont == LL_ADC_REG_SEQ_DISCONT_DISABLE));
+    }
+#endif /* ADC_SUPPORT_2_5_MSPS */
+    assert_param(IS_LL_ADC_REG_CONTINUOUS_MODE(ADC_REG_InitStruct->ContinuousMode));
+    assert_param(IS_LL_ADC_REG_DMA_TRANSFER(ADC_REG_InitStruct->DMATransfer));
+    assert_param(IS_LL_ADC_REG_OVR_DATA_BEHAVIOR(ADC_REG_InitStruct->Overrun));
+
+    /* Note: Hardware constraint (refer to description of this function):       */
+    /*       ADC instance must be disabled.                                     */
+    if(LL_ADC_IsEnabled(ADCx) == 0UL) {
+        /* Configuration of ADC hierarchical scope:                               */
+        /*  - ADC group regular                                                   */
+        /*    - Set ADC group regular trigger source                              */
+        /*    - Set ADC group regular sequencer length                            */
+        /*    - Set ADC group regular sequencer discontinuous mode                */
+        /*    - Set ADC group regular continuous mode                             */
+        /*    - Set ADC group regular conversion data transfer: no transfer or    */
+        /*      transfer by DMA, and DMA requests mode                            */
+        /*    - Set ADC group regular overrun behavior                            */
+        /* Note: On this STM32 series, ADC trigger edge is set to value 0x0 by     */
+        /*       setting of trigger source to SW start.                           */
+#if defined(ADC_SUPPORT_2_5_MSPS)
+        if((LL_ADC_REG_GetSequencerConfigurable(ADCx) == LL_ADC_REG_SEQ_FIXED) ||
+           (ADC_REG_InitStruct->SequencerLength != LL_ADC_REG_SEQ_SCAN_DISABLE)) {
+            /* Case of sequencer mode fixed
+               or sequencer length >= 2 ranks with sequencer mode fully configurable:
+               discontinuous mode configured */
+            MODIFY_REG(
+                ADCx->CFGR1,
+                ADC_CFGR1_EXTSEL | ADC_CFGR1_EXTEN | ADC_CFGR1_DISCEN | ADC_CFGR1_CONT |
+                    ADC_CFGR1_DMAEN | ADC_CFGR1_DMACFG | ADC_CFGR1_OVRMOD,
+                ADC_REG_InitStruct->TriggerSource | ADC_REG_InitStruct->SequencerDiscont |
+                    ADC_REG_InitStruct->ContinuousMode | ADC_REG_InitStruct->DMATransfer |
+                    ADC_REG_InitStruct->Overrun);
+        } else {
+            /* Case of sequencer mode fully configurable
+               and sequencer length 1 rank (sequencer disabled):
+               discontinuous mode discarded (fixed to disable) */
+            MODIFY_REG(
+                ADCx->CFGR1,
+                ADC_CFGR1_EXTSEL | ADC_CFGR1_EXTEN | ADC_CFGR1_DISCEN | ADC_CFGR1_CONT |
+                    ADC_CFGR1_DMAEN | ADC_CFGR1_DMACFG | ADC_CFGR1_OVRMOD,
+                ADC_REG_InitStruct->TriggerSource | LL_ADC_REG_SEQ_DISCONT_DISABLE |
+                    ADC_REG_InitStruct->ContinuousMode | ADC_REG_InitStruct->DMATransfer |
+                    ADC_REG_InitStruct->Overrun);
+        }
+#else
+        if(ADC_REG_InitStruct->SequencerLength != LL_ADC_REG_SEQ_SCAN_DISABLE) {
+            MODIFY_REG(
+                ADCx->CFGR,
+                ADC_CFGR_EXTSEL | ADC_CFGR_EXTEN | ADC_CFGR_DISCEN | ADC_CFGR_DISCNUM |
+                    ADC_CFGR_CONT | ADC_CFGR_DMAEN | ADC_CFGR_DMACFG | ADC_CFGR_OVRMOD,
+                ADC_REG_InitStruct->TriggerSource | ADC_REG_InitStruct->SequencerDiscont |
+                    ADC_REG_InitStruct->ContinuousMode | ADC_REG_InitStruct->DMATransfer |
+                    ADC_REG_InitStruct->Overrun);
+        } else {
+            MODIFY_REG(
+                ADCx->CFGR,
+                ADC_CFGR_EXTSEL | ADC_CFGR_EXTEN | ADC_CFGR_DISCEN | ADC_CFGR_DISCNUM |
+                    ADC_CFGR_CONT | ADC_CFGR_DMAEN | ADC_CFGR_DMACFG | ADC_CFGR_OVRMOD,
+                ADC_REG_InitStruct->TriggerSource | LL_ADC_REG_SEQ_DISCONT_DISABLE |
+                    ADC_REG_InitStruct->ContinuousMode | ADC_REG_InitStruct->DMATransfer |
+                    ADC_REG_InitStruct->Overrun);
+        }
+#endif /* ADC_SUPPORT_2_5_MSPS */
+
+        /* Set ADC group regular sequencer length and scan direction */
+#if defined(ADC_SUPPORT_2_5_MSPS)
+        if(LL_ADC_REG_GetSequencerConfigurable(ADCx) != LL_ADC_REG_SEQ_FIXED) {
+            LL_ADC_REG_SetSequencerLength(ADCx, ADC_REG_InitStruct->SequencerLength);
+        }
+#else
+        LL_ADC_REG_SetSequencerLength(ADCx, ADC_REG_InitStruct->SequencerLength);
+#endif /* ADC_SUPPORT_2_5_MSPS */
+    } else {
+        /* Initialization error: ADC instance is not disabled. */
+        status = ERROR;
+    }
+    return status;
+}
+
+ErrorStatus LL_ADC_CommonInit(
+    ADC_Common_TypeDef* ADCxy_COMMON,
+    const LL_ADC_CommonInitTypeDef* ADC_CommonInitStruct) {
+    ErrorStatus status = SUCCESS;
+
+    /* Check the parameters */
+    assert_param(IS_ADC_COMMON_INSTANCE(ADCxy_COMMON));
+    assert_param(IS_LL_ADC_COMMON_CLOCK(ADC_CommonInitStruct->CommonClock));
+
+    /* Note: Hardware constraint (refer to description of functions             */
+    /*       "LL_ADC_SetCommonXXX()":                                           */
+    /*       On this STM32 series, setting of these features is conditioned to   */
+    /*       ADC state:                                                         */
+    /*       All ADC instances of the ADC common group must be disabled.        */
+    if(__LL_ADC_IS_ENABLED_ALL_COMMON_INSTANCE(ADCxy_COMMON) == 0UL) {
+        /* Configuration of ADC hierarchical scope:                               */
+        /*  - common to several ADC                                               */
+        /*    (all ADC instances belonging to the same ADC common instance)       */
+        /*    - Set ADC clock (conversion clock)                                  */
+#if defined(ADC_MULTIMODE_SUPPORT)
+        if(ADC_CommonInitStruct->Multimode != LL_ADC_MULTI_INDEPENDENT) {
+            MODIFY_REG(
+                ADCxy_COMMON->CCR,
+                ADC_CCR_CKMODE | ADC_CCR_PRESC | ADC_CCR_DUAL | ADC_CCR_MDMA | ADC_CCR_DELAY,
+                ADC_CommonInitStruct->CommonClock | ADC_CommonInitStruct->Multimode |
+                    ADC_CommonInitStruct->MultiDMATransfer |
+                    ADC_CommonInitStruct->MultiTwoSamplingDelay);
+        } else {
+            MODIFY_REG(
+                ADCxy_COMMON->CCR,
+                ADC_CCR_CKMODE | ADC_CCR_PRESC | ADC_CCR_DUAL | ADC_CCR_MDMA | ADC_CCR_DELAY,
+                ADC_CommonInitStruct->CommonClock | LL_ADC_MULTI_INDEPENDENT);
+        }
+#else
+        LL_ADC_SetCommonClock(ADCxy_COMMON, ADC_CommonInitStruct->CommonClock);
+#endif
+    } else {
+        /* Initialization error: One or several ADC instances belonging to        */
+        /* the same ADC common instance are not disabled.                         */
+        status = ERROR;
+    }
+
+    return status;
+}

+ 30 - 0
oscilloscope/scenes/scope_scene.c

@@ -0,0 +1,30 @@
+#include "../scope_app_i.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const scope_scene_on_enter_handlers[])(void*) = {
+#include "scope_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const scope_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "scope_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const scope_scene_on_exit_handlers[])(void* context) = {
+#include "scope_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers scope_scene_handlers = {
+    .on_enter_handlers = scope_scene_on_enter_handlers,
+    .on_event_handlers = scope_scene_on_event_handlers,
+    .on_exit_handlers = scope_scene_on_exit_handlers,
+    .scene_num = ScopeSceneNum,
+};

+ 29 - 0
oscilloscope/scenes/scope_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) ScopeScene##id,
+typedef enum {
+#include "scope_scene_config.h"
+    ScopeSceneNum,
+} ScopeScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers scope_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "scope_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
+#include "scope_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
+#include "scope_scene_config.h"
+#undef ADD_SCENE

+ 60 - 0
oscilloscope/scenes/scope_scene_about.c

@@ -0,0 +1,60 @@
+#include "../scope_app_i.h"
+
+void scope_scene_about_widget_callback(GuiButtonType result, InputType type, void* context) {
+    ScopeApp* app = context;
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, result);
+    }
+}
+
+void scope_scene_about_on_enter(void* context) {
+    ScopeApp* app = context;
+
+    FuriString* temp_str;
+    temp_str = furi_string_alloc();
+    furi_string_printf(temp_str, "\e#%s\n", "Information");
+    furi_string_cat_printf(
+        temp_str,
+        "Provide signal to pin 16/PC0, with a voltage ranging from 0V to 2.5V and ground to pin 18/GND.\n\n");
+    furi_string_cat_printf(temp_str, "Developed by: %s\n", S_DEVELOPED);
+    furi_string_cat_printf(temp_str, "Github: %s\n\n", S_GITHUB);
+
+    widget_add_text_box_element(
+        app->widget,
+        0,
+        0,
+        128,
+        14,
+        AlignCenter,
+        AlignBottom,
+        "\e#\e!                                                      \e!\n",
+        false);
+    widget_add_text_box_element(
+        app->widget,
+        0,
+        2,
+        128,
+        14,
+        AlignCenter,
+        AlignBottom,
+        "\e#\e!          flipperscope             \e!\n",
+        false);
+    widget_add_text_scroll_element(app->widget, 0, 16, 128, 50, furi_string_get_cstr(temp_str));
+    furi_string_free(temp_str);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, ScopeViewWidget);
+}
+
+bool scope_scene_about_on_event(void* context, SceneManagerEvent event) {
+    ScopeApp* app = context;
+    bool consumed = false;
+    UNUSED(app);
+    UNUSED(event);
+    return consumed;
+}
+
+void scope_scene_about_on_exit(void* context) {
+    ScopeApp* app = context;
+    // Clear views
+    widget_reset(app->widget);
+}

+ 5 - 0
oscilloscope/scenes/scope_scene_config.h

@@ -0,0 +1,5 @@
+ADD_SCENE(scope, start, Start)
+ADD_SCENE(scope, run, Run)
+ADD_SCENE(scope, save, Save)
+ADD_SCENE(scope, setup, Setup)
+ADD_SCENE(scope, about, About)

+ 594 - 0
oscilloscope/scenes/scope_scene_run.c

@@ -0,0 +1,594 @@
+#include <float.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_resources.h>
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/widget.h>
+#include <gui/elements.h>
+#include <notification/notification_messages.h>
+
+#include "stm32wbxx_ll_adc.h"
+#include "stm32wbxx_ll_dma.h"
+#include "stm32wbxx_ll_crs.h"
+#include "stm32wbxx_ll_rcc.h"
+#include "stm32wbxx_ll_bus.h"
+#include "stm32wbxx_ll_system.h"
+#include "stm32wbxx_ll_exti.h"
+#include "stm32wbxx_ll_cortex.h"
+#include "stm32wbxx_ll_utils.h"
+#include "stm32wbxx_ll_pwr.h"
+#include "stm32wbxx_ll_tim.h"
+#include "stm32wbxx_ll_gpio.h"
+
+#include "../scope_app_i.h"
+#include "flipperscope_icons.h"
+
+#define DIGITAL_SCALE_12BITS ((uint32_t)0xFFF)
+#define VAR_CONVERTED_DATA_INIT_VALUE (DIGITAL_SCALE_12BITS + 1)
+#define VAR_CONVERTED_DATA_INIT_VALUE_16BITS (0xFFFF + 1U)
+#define __ADC_CALC_DATA_VOLTAGE(__VREFANALOG_VOLTAGE__, __ADC_DATA__) \
+    ((__ADC_DATA__) * (__VREFANALOG_VOLTAGE__) / DIGITAL_SCALE_12BITS)
+#define VDDA_APPLI ((uint32_t)2500)
+#define TIMER_FREQUENCY_RANGE_MIN (1UL)
+#define TIMER_PRESCALER_MAX_VALUE (0xFFFF - 1UL)
+#define ADC_DELAY_CALIB_ENABLE_CPU_CYCLES (LL_ADC_DELAY_CALIB_ENABLE_ADC_CYCLES * 32)
+
+// ramVector found from - https://community.nxp.com/t5/i-MX-Processors/Relocate-vector-table-to-ITCM/m-p/1302304
+// the aligned aspect is key!
+#define TABLE_SIZE 79
+uint32_t ramVector[TABLE_SIZE + 1] __attribute__((aligned(512)));
+
+const uint32_t AHBPrescTable[16UL] =
+    {1UL, 3UL, 5UL, 1UL, 1UL, 6UL, 10UL, 32UL, 2UL, 4UL, 8UL, 16UL, 64UL, 128UL, 256UL, 512UL};
+const uint32_t APBPrescTable[8UL] = {0UL, 0UL, 0UL, 0UL, 1UL, 2UL, 3UL, 4UL};
+const uint32_t MSIRangeTable[16UL] = {
+    100000UL,
+    200000UL,
+    400000UL,
+    800000UL,
+    1000000UL,
+    2000000UL,
+    4000000UL,
+    8000000UL,
+    16000000UL,
+    24000000UL,
+    32000000UL,
+    48000000UL,
+    0UL,
+    0UL,
+    0UL,
+    0UL}; /* 0UL values are incorrect cases */
+
+char* time; // Current time period text
+double freq; // Current samplerate
+uint8_t pause = 0; // Whether we want to pause output or not
+enum measureenum type; // Type of measurement we are performing
+int toggle = 0; // Used for toggling output GPIO, only used in testing
+
+void Error_Handler() {
+    while(1) {
+    }
+}
+
+__IO uint16_t
+    aADCxConvertedData[ADC_CONVERTED_DATA_BUFFER_SIZE]; // Array that ADC data is copied to, via DMA
+__IO uint16_t aADCxConvertedData_Voltage_mVoltA
+    [ADC_CONVERTED_DATA_BUFFER_SIZE]; // Data is converted to range from 0 to 2500
+__IO uint16_t aADCxConvertedData_Voltage_mVoltB
+    [ADC_CONVERTED_DATA_BUFFER_SIZE]; // Data is converted to range from 0 to 2500
+__IO uint8_t ubDmaTransferStatus = 2; // DMA transfer status
+
+__IO uint16_t* mvoltWrite =
+    &aADCxConvertedData_Voltage_mVoltA[0]; // Pointer to area we write converted voltage data to
+__IO uint16_t* mvoltDisplay =
+    &aADCxConvertedData_Voltage_mVoltB[0]; // Pointer to area of memory we display
+
+void AdcDmaTransferComplete_Callback();
+void AdcDmaTransferHalf_Callback();
+
+void AdcGrpRegularOverrunError_Callback(void) {
+    LL_ADC_DisableIT_OVR(ADC1);
+}
+
+void AdcDmaTransferError_Callback() {
+}
+
+void DMA1_Channel1_IRQHandler(void) {
+    if(LL_DMA_IsActiveFlag_TC1(DMA1) == 1) {
+        LL_DMA_ClearFlag_TC1(DMA1);
+        AdcDmaTransferComplete_Callback();
+    }
+
+    if(LL_DMA_IsActiveFlag_HT1(DMA1) == 1) {
+        LL_DMA_ClearFlag_HT1(DMA1);
+        AdcDmaTransferHalf_Callback();
+    }
+
+    if(LL_DMA_IsActiveFlag_TE1(DMA1) == 1) {
+        LL_DMA_ClearFlag_TE1(DMA1);
+        AdcDmaTransferError_Callback();
+    }
+}
+
+void ADC1_IRQHandler(void) {
+    if(LL_ADC_IsActiveFlag_OVR(ADC1) != 0) {
+        LL_ADC_ClearFlag_OVR(ADC1);
+        AdcGrpRegularOverrunError_Callback();
+    }
+}
+
+void TIM2_IRQHandler(void) {
+}
+
+static void MX_ADC1_Init(void) {
+    LL_ADC_CommonInitTypeDef ADC_CommonInitStruct = {0};
+    LL_ADC_InitTypeDef ADC_InitStruct = {0};
+    LL_ADC_REG_InitTypeDef ADC_REG_InitStruct = {0};
+    LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
+
+    LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_ADC);
+    LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOC);
+    GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
+    GPIO_InitStruct.Mode = LL_GPIO_MODE_ANALOG;
+    GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
+    LL_GPIO_Init(GPIOC, &GPIO_InitStruct);
+
+    LL_DMA_SetPeriphRequest(DMA1, LL_DMA_CHANNEL_1, LL_DMAMUX_REQ_ADC1);
+    LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
+    LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PRIORITY_HIGH);
+    LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_CIRCULAR);
+    LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PERIPH_NOINCREMENT);
+    LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MEMORY_INCREMENT);
+    LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PDATAALIGN_HALFWORD);
+    LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MDATAALIGN_HALFWORD);
+
+    LL_DMAMUX_SetRequestID(DMAMUX1, LL_DMAMUX_CHANNEL_0, LL_DMAMUX_REQ_ADC1);
+
+    LL_DMA_ConfigAddresses(
+        DMA1,
+        LL_DMA_CHANNEL_1,
+        LL_ADC_DMA_GetRegAddr(ADC1, LL_ADC_DMA_REG_REGULAR_DATA),
+        (uint32_t)&aADCxConvertedData,
+        LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
+
+    LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, ADC_CONVERTED_DATA_BUFFER_SIZE);
+
+    LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1);
+    LL_DMA_EnableIT_HT(DMA1, LL_DMA_CHANNEL_1);
+    LL_DMA_EnableIT_TE(DMA1, LL_DMA_CHANNEL_1);
+
+    LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);
+    NVIC_SetPriority(ADC1_IRQn, 0);
+    NVIC_EnableIRQ(ADC1_IRQn);
+
+    ADC_CommonInitStruct.CommonClock = LL_ADC_CLOCK_SYNC_PCLK_DIV2;
+    LL_ADC_CommonInit(__LL_ADC_COMMON_INSTANCE(ADC1), &ADC_CommonInitStruct);
+    ADC_InitStruct.Resolution = LL_ADC_RESOLUTION_12B;
+    ADC_InitStruct.DataAlignment = LL_ADC_DATA_ALIGN_RIGHT;
+    ADC_InitStruct.LowPowerMode = LL_ADC_LP_MODE_NONE;
+    LL_ADC_Init(ADC1, &ADC_InitStruct);
+    ADC_REG_InitStruct.TriggerSource = LL_ADC_REG_TRIG_EXT_TIM2_TRGO;
+    ADC_REG_InitStruct.SequencerLength = LL_ADC_REG_SEQ_SCAN_DISABLE;
+    ADC_REG_InitStruct.SequencerDiscont = LL_ADC_REG_SEQ_DISCONT_DISABLE;
+    ADC_REG_InitStruct.ContinuousMode = LL_ADC_REG_CONV_SINGLE;
+    ADC_REG_InitStruct.DMATransfer = LL_ADC_REG_DMA_TRANSFER_UNLIMITED;
+    ADC_REG_InitStruct.Overrun = LL_ADC_REG_OVR_DATA_OVERWRITTEN;
+    LL_ADC_REG_Init(ADC1, &ADC_REG_InitStruct);
+    LL_ADC_SetOverSamplingScope(ADC1, LL_ADC_OVS_DISABLE);
+    LL_ADC_REG_SetTriggerEdge(ADC1, LL_ADC_REG_TRIG_EXT_FALLING);
+
+    LL_ADC_DisableDeepPowerDown(ADC1);
+    LL_ADC_EnableInternalRegulator(ADC1);
+    uint32_t wait_loop_index;
+    wait_loop_index =
+        ((LL_ADC_DELAY_INTERNAL_REGUL_STAB_US * (SystemCoreClock / (100000 * 2))) / 10);
+    while(wait_loop_index != 0) {
+        wait_loop_index--;
+    }
+
+    LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_1, LL_ADC_CHANNEL_1);
+    LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_1, LL_ADC_SAMPLINGTIME_247CYCLES_5);
+    LL_ADC_SetChannelSingleDiff(ADC1, LL_ADC_CHANNEL_1, LL_ADC_SINGLE_ENDED);
+    LL_ADC_EnableIT_OVR(ADC1);
+}
+
+double abs_error(double num1, double num2) {
+    return fabs((num1 - num2) / num1);
+}
+
+static void MX_TIM2_Init(int freq) {
+    uint32_t timer_clock_frequency = 0; /* Timer clock frequency */
+    uint32_t timer_prescaler =
+        0; /* Time base prescaler to have timebase aligned on minimum frequency possible */
+    uint32_t timer_reload =
+        0; /* Timer reload value in function of timer prescaler to achieve time base period */
+
+    LL_TIM_InitTypeDef TIM_InitStruct = {0};
+    LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM2);
+
+    if(LL_RCC_GetAPB1Prescaler() == LL_RCC_APB1_DIV_1) {
+        timer_clock_frequency =
+            __LL_RCC_CALC_PCLK1_FREQ(SystemCoreClock, LL_RCC_GetAPB1Prescaler());
+    } else {
+        timer_clock_frequency =
+            (__LL_RCC_CALC_PCLK1_FREQ(SystemCoreClock, LL_RCC_GetAPB1Prescaler()) * 2);
+    }
+
+    //(PSC+1) * (ARR+1)
+    double calc = timer_clock_frequency / (1 / (1 / (double)freq));
+    double PSC;
+    double ARR;
+    double minerr = 10000;
+    for(int i = 1; i < 65536; i++) {
+        PSC = i - 1;
+        ARR = calc / (PSC + 1);
+        double error = abs_error((int)(ARR), ARR);
+        if(error < (double)0.001 && error < minerr && ARR - 1 > 0) {
+            timer_prescaler = PSC;
+            timer_reload = ARR - 1;
+            minerr = error;
+            break;
+        }
+    }
+
+    TIM_InitStruct.Prescaler = timer_prescaler;
+    TIM_InitStruct.CounterMode = LL_TIM_COUNTERMODE_UP;
+    TIM_InitStruct.Autoreload = timer_reload;
+    TIM_InitStruct.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1;
+    LL_TIM_Init(TIM2, &TIM_InitStruct);
+    LL_TIM_SetTriggerInput(TIM2, LL_TIM_TS_ITR0);
+    LL_TIM_SetSlaveMode(TIM2, LL_TIM_SLAVEMODE_DISABLED);
+    LL_TIM_DisableIT_TRIG(TIM2);
+    LL_TIM_DisableDMAReq_TRIG(TIM2);
+    LL_TIM_SetTriggerOutput(TIM2, LL_TIM_TRGO_UPDATE);
+    LL_TIM_DisableMasterSlaveMode(TIM2);
+    LL_TIM_SetTriggerOutput(TIM2, LL_TIM_TRGO_UPDATE);
+    LL_TIM_EnableCounter(TIM2);
+}
+
+static void MX_DMA_Init(void) {
+    LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMAMUX1);
+    LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);
+    NVIC_SetPriority(DMA1_Channel1_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 1, 0));
+    NVIC_EnableIRQ(DMA1_Channel1_IRQn);
+}
+
+static void MX_GPIO_Init(void) {
+    LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOA);
+    LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOB);
+    LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOC);
+}
+
+// Swap pointer addresses, used for double buffer
+void swap(__IO uint16_t** a, __IO uint16_t** b) {
+    __IO uint16_t* tmp;
+    tmp = *a;
+    *a = *b;
+    *b = tmp;
+}
+
+void AdcDmaTransferComplete_Callback() {
+    uint32_t tmp_index = 0;
+    for(tmp_index = (ADC_CONVERTED_DATA_BUFFER_SIZE / 2);
+        tmp_index < ADC_CONVERTED_DATA_BUFFER_SIZE;
+        tmp_index++) {
+        mvoltWrite[tmp_index] = __LL_ADC_CALC_DATA_TO_VOLTAGE(
+            VDDA_APPLI, aADCxConvertedData[tmp_index], LL_ADC_RESOLUTION_12B);
+    }
+    ubDmaTransferStatus = 1;
+    if(!pause) swap(&mvoltWrite, &mvoltDisplay);
+}
+
+void AdcDmaTransferHalf_Callback() {
+    uint32_t tmp_index = 0;
+    for(tmp_index = 0; tmp_index < (ADC_CONVERTED_DATA_BUFFER_SIZE / 2); tmp_index++) {
+        mvoltWrite[tmp_index] = __LL_ADC_CALC_DATA_TO_VOLTAGE(
+            VDDA_APPLI, aADCxConvertedData[tmp_index], LL_ADC_RESOLUTION_12B);
+    }
+    ubDmaTransferStatus = 0;
+}
+
+void Activate_ADC(void) {
+    __IO uint32_t wait_loop_index = 0U;
+#if(USE_TIMEOUT == 1)
+    uint32_t Timeout = 0U; /* Variable used for timeout management */
+#endif /* USE_TIMEOUT */
+    if(LL_ADC_IsEnabled(ADC1) == 0) {
+        LL_ADC_DisableDeepPowerDown(ADC1);
+        LL_ADC_EnableInternalRegulator(ADC1);
+        wait_loop_index =
+            ((LL_ADC_DELAY_INTERNAL_REGUL_STAB_US * (SystemCoreClock / (100000 * 2))) / 10);
+        while(wait_loop_index != 0) {
+            wait_loop_index--;
+        }
+        LL_ADC_StartCalibration(ADC1, LL_ADC_SINGLE_ENDED);
+
+#if(USE_TIMEOUT == 1)
+        Timeout = ADC_CALIBRATION_TIMEOUT_MS;
+#endif /* USE_TIMEOUT */
+
+        while(LL_ADC_IsCalibrationOnGoing(ADC1) != 0) {
+#if(USE_TIMEOUT == 1)
+            if(LL_SYSTICK_IsActiveCounterFlag()) {
+                if(Timeout-- == 0) {
+                }
+            }
+#endif /* USE_TIMEOUT */
+        }
+        wait_loop_index = (ADC_DELAY_CALIB_ENABLE_CPU_CYCLES >> 1);
+        while(wait_loop_index != 0) {
+            wait_loop_index--;
+        }
+        LL_ADC_Enable(ADC1);
+#if(USE_TIMEOUT == 1)
+        Timeout = ADC_ENABLE_TIMEOUT_MS;
+#endif /* USE_TIMEOUT */
+        while(LL_ADC_IsActiveFlag_ADRDY(ADC1) == 0) {
+#if(USE_TIMEOUT == 1)
+            /* Check Systick counter flag to decrement the time-out value */
+            if(LL_SYSTICK_IsActiveCounterFlag()) {
+                if(Timeout-- == 0) {
+                    /* Time-out occurred. Set LED to blinking mode */
+                    LED_Blinking(LED_BLINK_ERROR);
+                }
+            }
+#endif /* USE_TIMEOUT */
+        }
+    }
+}
+
+// Used to draw to display
+static void app_draw_callback(Canvas* canvas, void* ctx) {
+    UNUSED(ctx);
+    static int16_t index[ADC_CONVERTED_DATA_BUFFER_SIZE];
+    static float data[ADC_CONVERTED_DATA_BUFFER_SIZE];
+    static float crossings[ADC_CONVERTED_DATA_BUFFER_SIZE];
+    static char buf1[50];
+
+    float max = 0.0;
+    float min = FLT_MAX;
+    int count = 0;
+
+    if(type == m_capture) {
+        if(!pause)
+            elements_button_center(canvas, "Stop");
+        else {
+            elements_button_center(canvas, "REC");
+            elements_button_right(canvas, "Save");
+        }
+    }
+
+    if(pause)
+        canvas_draw_icon(canvas, 115, 0, &I_pause_10x10);
+    else
+        canvas_draw_icon(canvas, 115, 0, &I_play_10x10);
+
+    // Calculate voltage measurements
+    for(uint32_t x = 0; x < ADC_CONVERTED_DATA_BUFFER_SIZE; x++) {
+        if(mvoltDisplay[x] < min) min = mvoltDisplay[x];
+        if(mvoltDisplay[x] > max) max = mvoltDisplay[x];
+    }
+    max /= 1000;
+    min /= 1000;
+
+    switch(type) {
+    case m_time: {
+        // Display current time period
+        snprintf(buf1, 50, "Time: %s", time);
+        canvas_draw_str(canvas, 10, 10, buf1);
+        // Shift waveform across a virtual 0 line, so it crosses 0
+        for(uint32_t x = 0; x < ADC_CONVERTED_DATA_BUFFER_SIZE; x++) {
+            index[x] = -1;
+            crossings[x] = -1.0;
+            data[x] = ((float)mvoltDisplay[x] / 1000) - min;
+            data[x] = ((2 / (max - min)) * data[x]) - 1;
+        }
+        // Find points at which waveform crosses virtual 0 line
+        for(uint32_t x = 1; x < ADC_CONVERTED_DATA_BUFFER_SIZE; x++) {
+            if(data[x] >= 0 && data[x - 1] < 0) {
+                index[count++] = x - 1;
+            }
+        }
+        count = 0;
+        // Linear interpolation to find zero crossings
+        // see https://gist.github.com/endolith/255291 for Python version
+        for(uint32_t x = 0; x < ADC_CONVERTED_DATA_BUFFER_SIZE; x++) {
+            if(index[x] == -1) break;
+            crossings[count++] =
+                (float)index[x] - data[index[x]] / (data[index[x] + 1] - data[index[x]]);
+        }
+        float avg = 0.0;
+        float countv = 0.0;
+        for(uint32_t x = 0; x < ADC_CONVERTED_DATA_BUFFER_SIZE; x++) {
+            if(x + 1 >= ADC_CONVERTED_DATA_BUFFER_SIZE) break;
+            if(crossings[x] == -1 || crossings[x + 1] == -1) break;
+            avg += crossings[x + 1] - crossings[x];
+            countv += 1;
+        }
+        avg /= countv;
+        // Display frequency of waveform
+        snprintf(buf1, 50, "Freq: %.1f Hz", (double)((float)freq / avg));
+        canvas_draw_str(canvas, 10, 20, buf1);
+    } break;
+    case m_voltage: {
+        // Display max, min, peak-to-peak voltages
+        snprintf(buf1, 50, "Max: %.2fV", (double)max);
+        canvas_draw_str(canvas, 10, 10, buf1);
+        snprintf(buf1, 50, "Min: %.2fV", (double)min);
+        canvas_draw_str(canvas, 10, 20, buf1);
+        snprintf(buf1, 50, "Vpp: %.2fV", (double)(max - min));
+        canvas_draw_str(canvas, 10, 30, buf1);
+    } break;
+    default:
+        break;
+    }
+
+    // Draw lines between each data point
+    for(uint32_t x = 1; x < ADC_CONVERTED_DATA_BUFFER_SIZE; x++) {
+        uint32_t prev = 64 - (mvoltDisplay[x - 1] / (VDDA_APPLI / 64));
+        uint32_t cur = 64 - (mvoltDisplay[x] / (VDDA_APPLI / 64));
+        canvas_draw_line(canvas, x - 1, prev, x, cur);
+    }
+
+    // Draw graph lines
+    canvas_draw_line(canvas, 0, 0, 0, 63);
+    canvas_draw_line(canvas, 0, 63, 128, 63);
+}
+
+static void app_input_callback(InputEvent* input_event, void* ctx) {
+    furi_assert(ctx);
+    FuriMessageQueue* event_queue = ctx;
+    furi_message_queue_put(event_queue, input_event, FuriWaitForever);
+}
+
+void scope_scene_run_on_enter(void* context) {
+    ScopeApp* app = context;
+
+    // Find string representation of time period we're using
+    for(uint32_t i = 0; i < COUNT_OF(time_list); i++) {
+        if(time_list[i].time == app->time) {
+            time = time_list[i].str;
+            break;
+        }
+    }
+
+    // Currently un-paused
+    pause = 0;
+
+    // What type of measurement are we performing
+    type = app->measurement;
+
+    // Copy vector table, modify to use our own IRQ handlers
+    __disable_irq();
+    memcpy(ramVector, (uint32_t*)(FLASH_BASE | SCB->VTOR), sizeof(uint32_t) * TABLE_SIZE);
+    SCB->VTOR = (uint32_t)ramVector;
+    ramVector[27] = (uint32_t)DMA1_Channel1_IRQHandler;
+    ramVector[34] = (uint32_t)ADC1_IRQHandler;
+    ramVector[44] = (uint32_t)TIM2_IRQHandler;
+    __enable_irq();
+
+    furi_hal_bus_enable(FuriHalBusTIM2);
+
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+
+    uint32_t tmp_index_adc_converted_data = 0;
+    MX_GPIO_Init();
+    MX_DMA_Init();
+
+    freq = 1 / app->time;
+    MX_TIM2_Init((int)freq);
+
+    // Set VREFBUF to 2.5V, as vref isn't connected to 3.3V itself in the flipper zero
+    VREFBUF->CSR |= VREFBUF_CSR_ENVR;
+    VREFBUF->CSR &= ~VREFBUF_CSR_HIZ;
+    VREFBUF->CSR |= VREFBUF_CSR_VRS;
+    while(!(VREFBUF->CSR & VREFBUF_CSR_VRR)) {
+    };
+
+    MX_ADC1_Init();
+
+    // Setup initial values from ADC
+    for(tmp_index_adc_converted_data = 0;
+        tmp_index_adc_converted_data < ADC_CONVERTED_DATA_BUFFER_SIZE;
+        tmp_index_adc_converted_data++) {
+        aADCxConvertedData[tmp_index_adc_converted_data] = VAR_CONVERTED_DATA_INIT_VALUE;
+        aADCxConvertedData_Voltage_mVoltA[tmp_index_adc_converted_data] = 0;
+        aADCxConvertedData_Voltage_mVoltB[tmp_index_adc_converted_data] = 0;
+    }
+
+    Activate_ADC();
+
+    if((LL_ADC_IsEnabled(ADC1) == 1) && (LL_ADC_IsDisableOngoing(ADC1) == 0) &&
+       (LL_ADC_REG_IsConversionOngoing(ADC1) == 0)) {
+        LL_ADC_REG_StartConversion(ADC1);
+    }
+
+    ViewPort* view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, app_draw_callback, view_port);
+    view_port_input_callback_set(view_port, app_input_callback, event_queue);
+
+    // Register view port in GUI
+    Gui* gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+    InputEvent event;
+    bool running = true;
+    bool save = false;
+    while(running) {
+        if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) {
+            if((event.type == InputTypePress) || (event.type == InputTypeRepeat)) {
+                switch(event.key) {
+                case InputKeyLeft:
+                    break;
+                case InputKeyRight: {
+                    // Save data if in capture mode
+                    if(type == m_capture && pause == 1) {
+                        running = false;
+                        save = true;
+                    }
+                } break;
+                case InputKeyUp:
+                    break;
+                case InputKeyDown:
+                    break;
+                case InputKeyOk:
+                    pause ^= 1;
+                    break;
+                default:
+                    running = false;
+                    break;
+                }
+            }
+        }
+        view_port_update(view_port);
+    }
+
+    furi_hal_bus_disable(FuriHalBusTIM2);
+
+    // Disable ADC interrupt and timer
+    LL_ADC_DisableIT_OVR(ADC1);
+    LL_TIM_DisableCounter(TIM2);
+
+    // Stop DMA and switch back to original vector table
+    LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_1);
+
+    __disable_irq();
+    SCB->VTOR = 0;
+    __enable_irq();
+
+    if(!save) {
+        view_port_enabled_set(view_port, false);
+        gui_remove_view_port(gui, view_port);
+        view_port_free(view_port);
+
+        // Switch back to original scene
+        furi_record_close(RECORD_GUI);
+        scene_manager_previous_scene(app->scene_manager);
+        submenu_set_selected_item(app->submenu, 0);
+    } else {
+        view_port_enabled_set(view_port, false);
+        gui_remove_view_port(gui, view_port);
+        view_port_free(view_port);
+
+        app->data = malloc(sizeof(uint16_t) * ADC_CONVERTED_DATA_BUFFER_SIZE);
+        memcpy(
+            app->data, (uint16_t*)mvoltDisplay, sizeof(uint16_t) * ADC_CONVERTED_DATA_BUFFER_SIZE);
+        scene_manager_next_scene(app->scene_manager, ScopeSceneSave);
+    }
+}
+
+bool scope_scene_run_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void scope_scene_run_on_exit(void* context) {
+    ScopeApp* app = context;
+    // Clear views
+    widget_reset(app->widget);
+}

+ 85 - 0
oscilloscope/scenes/scope_scene_save.c

@@ -0,0 +1,85 @@
+#include <float.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_resources.h>
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/widget.h>
+#include <gui/elements.h>
+#include <notification/notification_messages.h>
+#include <flipper_format/flipper_format.h>
+#include "../scope_app_i.h"
+
+void scope_scene_save_text_input_callback(void* context) {
+    ScopeApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, ScopeCustomEventTextInputDone);
+}
+
+void scope_scene_save_on_enter(void* context) {
+    ScopeApp* app = context;
+    // Setup view
+    TextInput* text_input = app->text_input;
+    FuriString* file_name = furi_string_alloc();
+    FuriString* dir_name = furi_string_alloc();
+
+    text_input_set_header_text(text_input, "Name signal");
+    text_input_set_result_callback(
+        text_input,
+        scope_scene_save_text_input_callback,
+        app,
+        app->file_name_tmp,
+        MAX_LEN_NAME,
+        false);
+
+    furi_string_free(file_name);
+    furi_string_free(dir_name);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, ScopeViewSave);
+}
+
+bool scope_scene_save_on_event(void* context, SceneManagerEvent event) {
+    ScopeApp* app = context;
+    UNUSED(app);
+    UNUSED(event);
+
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom && event.event == ScopeCustomEventTextInputDone) {
+        if(strcmp(app->file_name_tmp, "") != 0) {
+            FuriString* temp_str = furi_string_alloc();
+            furi_string_printf(
+                temp_str,
+                "%s/%s%s",
+                APP_DATA_PATH(""),
+                app->file_name_tmp,
+                FLIPPERSCOPE_APP_EXTENSION);
+            Storage* storage = furi_record_open(RECORD_STORAGE);
+            File* file = storage_file_alloc(storage);
+            if(!storage_file_open(
+                   file, furi_string_get_cstr(temp_str), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
+                // Todo: Display error
+            }
+            if(!storage_file_write(
+                   file, app->data, sizeof(uint16_t) * ADC_CONVERTED_DATA_BUFFER_SIZE)) {
+                // Todo: Display error
+            }
+            storage_file_close(file);
+            storage_file_free(file);
+            furi_record_close(RECORD_STORAGE);
+            free(app->data);
+            app->data = NULL;
+            scene_manager_previous_scene(app->scene_manager);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void scope_scene_save_on_exit(void* context) {
+    ScopeApp* app = context;
+    // Clear views
+    text_input_reset(app->text_input);
+}

+ 68 - 0
oscilloscope/scenes/scope_scene_setup.c

@@ -0,0 +1,68 @@
+#include "../scope_app_i.h"
+
+void scope_scene_setup_widget_callback(GuiButtonType result, InputType type, void* context) {
+    ScopeApp* app = context;
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, result);
+    }
+}
+
+static void timeperiod_cb(VariableItem* item) {
+    ScopeApp* app = variable_item_get_context(item);
+    furi_assert(app);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, time_list[index].str);
+    app->time = time_list[index].time;
+}
+
+static void measurement_cb(VariableItem* item) {
+    ScopeApp* app = variable_item_get_context(item);
+    furi_assert(app);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, measurement_list[index].str);
+    app->measurement = measurement_list[index].type;
+}
+
+void scope_scene_setup_on_enter(void* context) {
+    ScopeApp* app = context;
+    VariableItemList* var_item_list = app->variable_item_list;
+    VariableItem* item;
+    item = variable_item_list_add(
+        var_item_list, "Time period", COUNT_OF(time_list), timeperiod_cb, app);
+
+    for(uint32_t i = 0; i < COUNT_OF(time_list); i++) {
+        if(time_list[i].time == app->time) {
+            variable_item_set_current_value_index(item, i);
+            variable_item_set_current_value_text(item, time_list[i].str);
+            break;
+        }
+    }
+
+    item = variable_item_list_add(
+        var_item_list, "Measurement", COUNT_OF(measurement_list), measurement_cb, app);
+
+    for(uint32_t i = 0; i < COUNT_OF(measurement_list); i++) {
+        if(measurement_list[i].type == app->measurement) {
+            variable_item_set_current_value_index(item, i);
+            variable_item_set_current_value_text(item, measurement_list[i].str);
+            break;
+        }
+    }
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, ScopeViewVariableItemList);
+}
+
+bool scope_scene_setup_on_event(void* context, SceneManagerEvent event) {
+    ScopeApp* app = context;
+    bool consumed = false;
+    UNUSED(app);
+    UNUSED(event);
+    return consumed;
+}
+
+void scope_scene_setup_on_exit(void* context) {
+    ScopeApp* app = context;
+    variable_item_list_reset(app->variable_item_list);
+    // Clear views
+    widget_reset(app->widget);
+}

+ 58 - 0
oscilloscope/scenes/scope_scene_start.c

@@ -0,0 +1,58 @@
+#include "../scope_app_i.h"
+
+typedef enum {
+    SubmenuIndexScopeRun,
+    SubmenuIndexScopeSetup,
+    SubmenuIndexScopeAbout,
+} SubmenuIndex;
+
+void scope_scene_start_submenu_callback(void* context, uint32_t index) {
+    ScopeApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void scope_scene_start_on_enter(void* context) {
+    UNUSED(context);
+    ScopeApp* app = context;
+    Submenu* submenu = app->submenu;
+
+    submenu_add_item(
+        submenu, "Run", SubmenuIndexScopeRun, scope_scene_start_submenu_callback, app);
+
+    submenu_add_item(
+        submenu, "Setup", SubmenuIndexScopeSetup, scope_scene_start_submenu_callback, app);
+
+    submenu_add_item(
+        submenu, "About", SubmenuIndexScopeAbout, scope_scene_start_submenu_callback, app);
+
+    submenu_set_selected_item(
+        submenu, scene_manager_get_scene_state(app->scene_manager, ScopeSceneStart));
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, ScopeViewSubmenu);
+}
+
+bool scope_scene_start_on_event(void* context, SceneManagerEvent event) {
+    ScopeApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexScopeAbout) {
+            scene_manager_next_scene(app->scene_manager, ScopeSceneAbout);
+            consumed = true;
+        } else if(event.event == SubmenuIndexScopeRun) {
+            scene_manager_next_scene(app->scene_manager, ScopeSceneRun);
+            consumed = true;
+        } else if(event.event == SubmenuIndexScopeSetup) {
+            scene_manager_next_scene(app->scene_manager, ScopeSceneSetup);
+            consumed = true;
+        }
+        scene_manager_set_scene_state(app->scene_manager, ScopeSceneStart, event.event);
+    }
+
+    return consumed;
+}
+
+void scope_scene_start_on_exit(void* context) {
+    ScopeApp* app = context;
+    submenu_reset(app->submenu);
+}

+ 14 - 0
oscilloscope/scenes/scope_types.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+
+#define S_DEVELOPED "anfractuosity"
+#define S_GITHUB "https://github.com/anfractuosity/flipperscope"
+
+typedef enum {
+    ScopeViewVariableItemList,
+    ScopeViewSubmenu,
+    ScopeViewWidget,
+    ScopeViewSave,
+} ScopeView;

+ 128 - 0
oscilloscope/scope.c

@@ -0,0 +1,128 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_resources.h>
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/widget.h>
+#include <notification/notification_messages.h>
+
+#include "scope_app_i.h"
+
+void assert_failed(uint8_t* file, uint32_t line) {
+    UNUSED(file);
+    UNUSED(line);
+    while(1) {
+    }
+}
+
+static bool scope_app_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    ScopeApp* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+static bool scope_app_back_event_callback(void* context) {
+    furi_assert(context);
+    ScopeApp* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+static void scope_app_tick_event_callback(void* context) {
+    furi_assert(context);
+    ScopeApp* app = context;
+    scene_manager_handle_tick_event(app->scene_manager);
+}
+
+ScopeApp* scope_app_alloc() {
+    ScopeApp* app = malloc(sizeof(ScopeApp));
+
+    // GUI
+    app->gui = furi_record_open(RECORD_GUI);
+
+    // View Dispatcher
+    app->view_dispatcher = view_dispatcher_alloc();
+    app->scene_manager = scene_manager_alloc(&scope_scene_handlers, app);
+    view_dispatcher_enable_queue(app->view_dispatcher);
+
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, scope_app_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, scope_app_back_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, scope_app_tick_event_callback, 100);
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    // Open Notification record
+    app->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    // Variable Item List
+    app->variable_item_list = variable_item_list_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        ScopeViewVariableItemList,
+        variable_item_list_get_view(app->variable_item_list));
+
+    // SubMenu
+    app->submenu = submenu_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, ScopeViewSubmenu, submenu_get_view(app->submenu));
+
+    // Widget
+    app->widget = widget_alloc();
+    view_dispatcher_add_view(app->view_dispatcher, ScopeViewWidget, widget_get_view(app->widget));
+
+    app->text_input = text_input_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, ScopeViewSave, text_input_get_view(app->text_input));
+
+    app->time = 0.001;
+    app->measurement = m_time;
+
+    scene_manager_next_scene(app->scene_manager, ScopeSceneStart);
+    return app;
+}
+
+void scope_app_free(ScopeApp* app) {
+    furi_assert(app);
+
+    // Submenu
+    view_dispatcher_remove_view(app->view_dispatcher, ScopeViewSubmenu);
+    submenu_free(app->submenu);
+
+    // Variable Item List
+    view_dispatcher_remove_view(app->view_dispatcher, ScopeViewVariableItemList);
+    variable_item_list_free(app->variable_item_list);
+
+    // Text input
+    view_dispatcher_remove_view(app->view_dispatcher, ScopeViewWidget);
+    text_input_free(app->text_input);
+
+    //  Widget
+    view_dispatcher_remove_view(app->view_dispatcher, ScopeViewSave);
+    widget_free(app->widget);
+
+    // View dispatcher
+    view_dispatcher_free(app->view_dispatcher);
+    scene_manager_free(app->scene_manager);
+
+    // Notifications
+    furi_record_close(RECORD_NOTIFICATION);
+    app->notifications = NULL;
+
+    // Close records
+    furi_record_close(RECORD_GUI);
+
+    free(app);
+}
+
+int32_t scope_main(void* p) {
+    UNUSED(p);
+    ScopeApp* scope_app = scope_app_alloc();
+    view_dispatcher_run(scope_app->view_dispatcher);
+    scope_app_free(scope_app);
+    return 0;
+}

BIN
oscilloscope/scope_10px.png


+ 58 - 0
oscilloscope/scope_app_i.h

@@ -0,0 +1,58 @@
+#pragma once
+
+#include "scenes/scope_types.h"
+#include "scenes/scope_scene.h"
+
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/text_input.h>
+#include <notification/notification_messages.h>
+
+#define ADC_CONVERTED_DATA_BUFFER_SIZE ((uint32_t)128)
+#define FLIPPERSCOPE_APP_EXTENSION ".dat"
+#define MAX_LEN_NAME 30
+
+typedef struct ScopeApp ScopeApp;
+
+typedef struct {
+    double time;
+    char* str;
+} timeperiod;
+
+static const timeperiod time_list[] =
+    {{1.0, "1s"}, {0.1, "0.1s"}, {1e-3, "1ms"}, {0.1e-3, "0.1ms"}, {1e-6, "1us"}};
+
+enum measureenum { m_time, m_voltage, m_capture };
+
+typedef struct {
+    enum measureenum type;
+    char* str;
+} measurement;
+
+static const measurement measurement_list[] = {
+    {m_time, "Time"},
+    {m_voltage, "Voltage"},
+    {m_capture, "Capture"}};
+
+struct ScopeApp {
+    Gui* gui;
+    ViewDispatcher* view_dispatcher;
+    SceneManager* scene_manager;
+    NotificationApp* notifications;
+    VariableItemList* variable_item_list;
+    Submenu* submenu;
+    Widget* widget;
+    TextInput* text_input;
+    double time;
+    enum measureenum measurement;
+    char file_name_tmp[MAX_LEN_NAME];
+    uint16_t* data;
+};
+
+enum ScopeCustomEvent {
+    ScopeCustomEventTextInputDone,
+};