Skip to content

GPIO Signals

Derek Jamison edited this page Jan 5, 2024 · 27 revisions

General Purpose Input/Output

Signals

Bit-Banging and GPIO write

One approach is to use furi_hal_gpio_write to set the logic level of a pin. You can then delay using furi_delay_ms or furi_delay_us for the appropriate time until you transition the logic level again. One challenge with this approach is to remember that the instructions also take some amount of time, so delays in the us may not be accurate. Also, your process could get interrupted, so you may want to request the scheduler to not switch away from your process using furi_kernel_lock.

In this example, we use configure pin A7 as OutputPushPull. calling gpio_write with true will cause the pin to be 3.3 volts; while writing a false will cause the pin to be 0.0 volts. We create 20 pulses, and we wait 150ms between each toggle. 150+150 = 300ms, which is a frequency of 3.333 Hz with a duty cycle of 50%.

#include <furi_hal.h>

void bit_bang_with_gpio_write() {
  furi_hal_gpio_init(&gpio_ext_pa7, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
  for(int i = 0; i < 20; i++) {
    furi_hal_gpio_write(&gpio_ext_pa7, true);
    furi_delay_ms(150);
    furi_hal_gpio_write(&gpio_ext_pa7, false);
    furi_delay_ms(150);
  }

  // Uninit GPIO
  furi_hal_gpio_init(&gpio_ext_pa7, GpioModeAnalog, GpioPullNo, GpioSpeedVeryHigh);
}

Bit-Banging and Register write

Another approach is to use write the port's BSRR register to set the logic level of a pin. This is similar to the previous example; except we use a register to set (GPIO_BSRR_BS0_Pos) or reset (GPIO_BSRR_BR0_Pos) the logic level. In general, it's recommended what you use furi_hal_gpio_write instead of directly manipulating registers. Later when we learn about DMA, knowing that there is a memory address &gpio_ext_pa7.port->BSRR that you can write values to (gpio_ext_pa7.pin << GPIO_BSRR_BS0_Pos, gpio_ext_pa7.pin << GPIO_BSRR_BR0_Pos) can be helpful.

#include <furi_hal.h>

void bit_bang_with_register_write() {
  furi_hal_gpio_init(&gpio_ext_pa7, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
  for(int i = 0; i < 20; i++) {
    gpio_ext_pa7.port->BSRR = gpio_ext_pa7.pin << GPIO_BSRR_BS0_Pos;
    furi_delay_ms(150);
    gpio_ext_pa7.port->BSRR = gpio_ext_pa7.pin << GPIO_BSRR_BR0_Pos;
    furi_delay_ms(150);
  }

  // Uninit GPIO
  furi_hal_gpio_init(&gpio_ext_pa7, GpioModeAnalog, GpioPullNo, GpioSpeedVeryHigh);
}

FURI HAL PWM

There is a nice library if you want to output a pulse-width modulation (PWM) signal at a given frequency (in Hertz) and duty cycle. This API is easy to use, and it automatically initializes the GPIO pin A7 for output. Internally this uses TIM1 to generate the signal on CH1N using GpioAltFn1TIM1. This API allows you to start a PWM signal on pin A7 and then your code can do other things without having to worry about the pin. The disadvantage of this API is the frequency is an uint32_t, so you can only set integer amounts (like 3Hz or 4Hz) instead of some fractional speed like 3.333 Hz.

#include <furi.h>
#include <furi_hal_pwm.h>

void furi_hal_pwm() {
  furi_hal_pwm_start(FuriHalPwmOutputIdTim1PA7, 4, 50); // Pin A7, 4 Hz, 50% duty cycle
  furi_delay_ms(5000); // Wait 5 seconds
  furi_hal_pwm_stop(FuriHalPwmOutputIdTim1PA7); // Turn off pin A7.
}

Timer GPIO

You can use a Timer API to control GPIO. The Timer has Alternate functions that can be tied to particular GPIO pins. It is also possible to use interrupts (and the end of the timer) or Direct Memory Access (DMA) to toggle a GPIO pin, which may be required if the GPIO pin doesn't have a timer associated with it.

Pin A7 : GpioAltFn1TIM1 : TIM1, CH1N

Timer Toggle

Below is an example of using a timer (TIM1) with CH1N to control pin A7 in Toggle mode. The Flipper Zero runs with an internal clock at 64000000 Hz. We divide that frequency using DIV1, giving us the same 64000000 Hz. We then trigger with a prescaler of 64000-1, so every 64000 input we get one pulse. This means we have 1000 pulses per second (1 kHz). We set an Autoreload to 150-1 (so every 150 pulses, which is 150ms) we have an event. We set the event to OCMODE_TOGGLE so we toggle between HIGH and LOW output on CH1N. The result is a toggle every 150ms, so 150ms+150ms = 300ms, which is a frequency of 3.333 Hz with a duty cycle of 50%.

#include <stm32wbxx_ll_dma.h>

void toggle() {
    furi_hal_gpio_init_ex(
        &gpio_ext_pa7, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedVeryHigh, GpioAltFn1TIM1);

    furi_hal_bus_enable(FuriHalBusTIM1);

    LL_TIM_InitTypeDef tim_init = {
        .CounterMode = LL_TIM_COUNTERMODE_UP,
        .ClockDivision = LL_TIM_CLOCKDIVISION_DIV1,
        .Prescaler = 64000 - 1,
        .RepetitionCounter = 0,
        .Autoreload = 150 - 1,
    };
    LL_TIM_Init(TIM1, &tim_init);
    LL_TIM_SetClockSource(TIM1, LL_TIM_CLOCKSOURCE_INTERNAL);
    LL_TIM_EnableARRPreload(TIM1);

    LL_TIM_OC_InitTypeDef tim_oc_init = {
        .OCMode = LL_TIM_OCMODE_TOGGLE,
        .OCNPolarity = LL_TIM_OCPOLARITY_HIGH,
        .OCNState = LL_TIM_OCSTATE_ENABLE,
        .OCNIdleState = LL_TIM_OCIDLESTATE_LOW,
        // In this example, we are only using CH1N so we don't need to set these...
        //.OCPolarity = LL_TIM_OCPOLARITY_HIGH,
        //.OCState = LL_TIM_OCSTATE_ENABLE,
        //.OCIdleState = LL_TIM_OCIDLESTATE_LOW,
    };
    LL_TIM_OC_Init(TIM1, LL_TIM_CHANNEL_CH1, &tim_oc_init);
    LL_TIM_CC_EnableChannel(TIM1, LL_TIM_CHANNEL_CH1N);
    LL_TIM_EnableCounter(TIM1);
    LL_TIM_EnableAllOutputs(TIM1);
}

void toggle_done() {
    // Uninit GPIO
    furi_hal_gpio_init(&gpio_ext_pa7, GpioModeAnalog, GpioPullNo, GpioSpeedVeryHigh);

    LL_TIM_DisableAllOutputs(TIM1);
    LL_TIM_DisableCounter(TIM1);
    furi_hal_bus_disable(FuriHalBusTIM1);
}

Timer PWM

Below is an example of using a timer (TIM1) with CH1N to control pin A7 in PWM mode. The Flipper Zero runs with an internal clock at 64000000 Hz. We divide that frequency using DIV1, giving us the same 64000000 Hz. We then trigger with a prescaler of 64000-1, so every 64000 input we get one pulse. This means we have 1000 pulses per second (1 kHz). We set an Autoreload to 300-1 (so every 300 pulses, which is 300ms) we have an event (the end of our PWM signal). We set the event to OCMODE_PWM1 so we have a PWM output on CH1N. We set the CompareValue to 1/2 of our Autoreload value, so when the count matches the CompareValue (on the 150th pulse) we will go from HIGH to LOW for the remainder of the cycle. The result is a PWM of 300ms, which is a frequency of 3.333 Hz with a duty cycle of 50% (due to our CompareValue being 50% of the Autoreload.)

#include <stm32wbxx_ll_dma.h>

void pwm() {
    furi_hal_gpio_init_ex(
        &gpio_ext_pa7, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedVeryHigh, GpioAltFn1TIM1);

    furi_hal_bus_enable(FuriHalBusTIM1);

    LL_TIM_InitTypeDef tim_init = {
        .CounterMode = LL_TIM_COUNTERMODE_UP,
        .ClockDivision = LL_TIM_CLOCKDIVISION_DIV1,
        .Prescaler = 64000 - 1,
        .RepetitionCounter = 0,
        .Autoreload = 300 - 1,
    };
    LL_TIM_Init(TIM1, &tim_init);
    LL_TIM_SetClockSource(TIM1, LL_TIM_CLOCKSOURCE_INTERNAL);
    LL_TIM_EnableARRPreload(TIM1);

    LL_TIM_OC_InitTypeDef tim_oc_init = {
        .CompareValue = tim_init.Autoreload / 2,
        .OCMode = LL_TIM_OCMODE_PWM1,
        .OCNPolarity = LL_TIM_OCPOLARITY_HIGH,
        .OCNState = LL_TIM_OCSTATE_ENABLE,
        .OCNIdleState = LL_TIM_OCIDLESTATE_LOW,
        // In this example, we are only using CH1N so we don't need to set these...
        //.OCPolarity = LL_TIM_OCPOLARITY_HIGH,
        //.OCState = LL_TIM_OCSTATE_ENABLE,
        //.OCIdleState = LL_TIM_OCIDLESTATE_LOW,
    };
    LL_TIM_OC_Init(TIM1, LL_TIM_CHANNEL_CH1, &tim_oc_init);
    // EnablePreload for the CompareValue to work...
    LL_TIM_OC_EnablePreload(TIM1, LL_TIM_CHANNEL_CH1N);
    LL_TIM_CC_EnableChannel(TIM1, LL_TIM_CHANNEL_CH1N);
    LL_TIM_EnableCounter(TIM1);
    LL_TIM_EnableAllOutputs(TIM1);
}

Toggle using DMA to update duration

In this example we use Direct Memory Access (DMA) to update the timer's ARR register (which is the Autoreload counter). We configure DMA to read the word (32-bit) values from a buffer, incrementing the index, running in a circular mode (when it gets to the end it loops back to the start of the buffer). We write to the AAR register` and don't increment (so each write is to the Autoreload counter). We do the DMA action each time TIM1 completes.

#include <stm32wbxx_ll_dma.h>

uint32_t durations[] = {100-1, 100-1, 200-1, 200-1, 500-1, 500-1, 1000-1, 1000-1};

void toggle_with_dma_duration() {
    LL_DMA_InitTypeDef dma_led_transition_timer = {
        .Direction = LL_DMA_DIRECTION_MEMORY_TO_PERIPH,
        .PeriphOrM2MSrcAddress = (uint32_t)&TIM1->ARR,
        .PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT,
        .PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_WORD,
        .MemoryOrM2MDstAddress = (uint32_t)durations,
        .MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT,
        .MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_WORD,
        .Mode = LL_DMA_MODE_CIRCULAR,
        .NbData = COUNT_OF(durations),
        .PeriphRequest = LL_DMAMUX_REQ_TIM1_UP,
        .Priority = LL_DMA_PRIORITY_HIGH,
    };
    LL_DMA_Init(DMA1, LL_DMA_CHANNEL_1, &dma_led_transition_timer);
    LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);

    furi_hal_gpio_init_ex(
        &gpio_ext_pa7, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedVeryHigh, GpioAltFn1TIM1);

    furi_hal_bus_enable(FuriHalBusTIM1);

    LL_TIM_InitTypeDef tim_init = {
        .CounterMode = LL_TIM_COUNTERMODE_UP,
        .ClockDivision = LL_TIM_CLOCKDIVISION_DIV1,
        .Prescaler = 64000 - 1,
        .RepetitionCounter = 0,
        .Autoreload = 100 - 1,
    };
    LL_TIM_Init(TIM1, &tim_init);
    LL_TIM_SetClockSource(TIM1, LL_TIM_CLOCKSOURCE_INTERNAL);
    LL_TIM_EnableARRPreload(TIM1);

    LL_TIM_OC_InitTypeDef tim_oc_init = {
        .OCMode = LL_TIM_OCMODE_TOGGLE,
        .OCNPolarity = LL_TIM_OCPOLARITY_HIGH,
        .OCNState = LL_TIM_OCSTATE_ENABLE,
        .OCNIdleState = LL_TIM_OCIDLESTATE_LOW,
    };
    LL_TIM_OC_Init(TIM1, LL_TIM_CHANNEL_CH1, &tim_oc_init);
    LL_TIM_EnableAllOutputs(TIM1);
    LL_TIM_EnableCounter(TIM1);
    LL_TIM_EnableUpdateEvent(TIM1);
    LL_TIM_EnableDMAReq_UPDATE(TIM1);
    LL_TIM_GenerateEvent_UPDATE(TIM1);
}

PWM using DMA to update duty cycle

In this example we use Direct Memory Access (DMA) to update the timer's CCR1 register (which is the CompareValue). We configure DMA to read the word (32-bit) values from a buffer, incrementing the index, running in a circular mode (when it gets to the end it loops back to the start of the buffer). We write to the CCR1 register` and don't increment (so each write is to the CompareValue counter). We do the DMA action each time TIM1 completes (each cycle we update the duty cycle).

#include <stm32wbxx_ll_dma.h>

uint32_t duty_cycles[] = {10-1, 20-1, 30-1, 40-1, 50-1, 60-1, 70-1, 80-1, 90-1, 100-1};

void pwm_dma_duty() {
    LL_DMA_InitTypeDef dma_led_transition_timer = {
        .Direction = LL_DMA_DIRECTION_MEMORY_TO_PERIPH,
        .PeriphOrM2MSrcAddress = (uint32_t)&TIM1->CCR1,
        .PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT,
        .PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_WORD,
        .MemoryOrM2MDstAddress = (uint32_t)duty_cycles,
        .MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT,
        .MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_WORD,
        .Mode = LL_DMA_MODE_CIRCULAR,
        .NbData = COUNT_OF(duty_cycles),
        .PeriphRequest = LL_DMAMUX_REQ_TIM1_UP,
        .Priority = LL_DMA_PRIORITY_HIGH,
    };
    LL_DMA_Init(DMA1, LL_DMA_CHANNEL_1, &dma_led_transition_timer);
    LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);

    furi_hal_gpio_init_ex(
        &gpio_ext_pa7, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedVeryHigh, GpioAltFn1TIM1);

    furi_hal_bus_enable(FuriHalBusTIM1);

    LL_TIM_InitTypeDef tim_init = {
        .CounterMode = LL_TIM_COUNTERMODE_UP,
        .ClockDivision = LL_TIM_CLOCKDIVISION_DIV1,
        .Prescaler = 64000 - 1,
        .RepetitionCounter = 0,
        .Autoreload = 100-1,
    };
    LL_TIM_Init(TIM1, &tim_init);
    LL_TIM_SetClockSource(TIM1, LL_TIM_CLOCKSOURCE_INTERNAL);
    LL_TIM_EnableARRPreload(TIM1);

    LL_TIM_OC_InitTypeDef tim_oc_init = {
        .OCMode = LL_TIM_OCMODE_PWM1,
        .CompareValue = 5, // set by DMA
        .OCNPolarity = LL_TIM_OCPOLARITY_HIGH,
        .OCNState = LL_TIM_OCSTATE_ENABLE,
        .OCNIdleState = LL_TIM_OCIDLESTATE_LOW,
    };
    LL_TIM_OC_Init(TIM1, LL_TIM_CHANNEL_CH1, &tim_oc_init);
    LL_TIM_OC_EnablePreload(TIM1, LL_TIM_CHANNEL_CH1); // TIM1_CCR1
    LL_TIM_EnableAllOutputs(TIM1);
    LL_TIM_EnableCounter(TIM1);
    LL_TIM_EnableUpdateEvent(TIM1);
    LL_TIM_EnableDMAReq_UPDATE(TIM1);
    LL_TIM_GenerateEvent_UPDATE(TIM1);
}

PWM using DMA to update duty cycle and ISR to update DMA buffer

In this example we use Direct Memory Access (DMA) to update the timer's CCR1 register (which is the CompareValue). We configure DMA to read the word (32-bit) values from a buffer, incrementing the index, running in a circular mode (when it gets to the end it loops back to the start of the buffer). We write to the CCR1 register` and don't increment (so each write is to the CompareValue counter). We do the DMA action each time TIM1 completes (each cycle we update the duty cycle).

The DMA buffer we are reading from is configured in our interrupt routine. At the Half-Transfer (HT) point we fill the inactive buffer with data. At the Transfer-Complete (TC) point we swap buffers that DMA1 CH1 is using.

#include <furi.h>
#include <furi_hal.h>
#include <stm32wbxx_ll_dma.h>

uint32_t buff1[8] = {99, 99, 99, 99, 99, 99, 99, 99};
uint32_t buff2[8] = {0};
volatile bool buff1_is_active = true;
volatile uint32_t delay = 100;

static void dma_isr(void* context) {
    UNUSED(context);

    if(LL_DMA_IsActiveFlag_HT1(DMA1)) {
        LL_DMA_ClearFlag_HT1(DMA1);
        delay += 100;
        if(delay > 400) {
            delay = 100;
        }
        uint32_t* buff = (!buff1_is_active) ? buff1 : buff2;
        for(size_t i = 0; i < COUNT_OF(buff1); i++) {
            buff[i] = delay - 1;
        }
    }

    if(LL_DMA_IsActiveFlag_TC1(DMA1)) {
        LL_DMA_ClearFlag_TC1(DMA1);
        buff1_is_active = !buff1_is_active;
        if(buff1_is_active) {
            LL_DMA_SetMemoryAddress(DMA1, LL_DMA_CHANNEL_1, (uint32_t)buff1);
        } else {
            LL_DMA_SetMemoryAddress(DMA1, LL_DMA_CHANNEL_1, (uint32_t)buff2);
        }
    }
}

void toggle_with_dma_duration_isr_buffer() {
    LL_DMA_InitTypeDef dma_led_transition_timer = {
        .Direction = LL_DMA_DIRECTION_MEMORY_TO_PERIPH,
        .PeriphOrM2MSrcAddress = (uint32_t)&TIM1->ARR,
        .PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT,
        .PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_WORD,
        .MemoryOrM2MDstAddress = (uint32_t)buff1,
        .MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT,
        .MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_WORD,
        .Mode = LL_DMA_MODE_CIRCULAR,
        .NbData = COUNT_OF(buff1),
        .PeriphRequest = LL_DMAMUX_REQ_TIM1_UP,
        .Priority = LL_DMA_PRIORITY_HIGH,
    };
    LL_DMA_Init(DMA1, LL_DMA_CHANNEL_1, &dma_led_transition_timer);
    LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);

    LL_DMA_ClearFlag_HT1(DMA1);
    LL_DMA_ClearFlag_TC1(DMA1);
    LL_DMA_EnableIT_HT(DMA1, LL_DMA_CHANNEL_1);
    LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1);
    furi_hal_interrupt_set_isr_ex(FuriHalInterruptIdDma1Ch1, 4, dma_isr, NULL);

    furi_hal_gpio_init_ex(
        &gpio_ext_pa7, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedVeryHigh, GpioAltFn1TIM1);

    furi_hal_bus_enable(FuriHalBusTIM1);

    LL_TIM_InitTypeDef tim_init = {
        .CounterMode = LL_TIM_COUNTERMODE_UP,
        .ClockDivision = LL_TIM_CLOCKDIVISION_DIV1,
        .Prescaler = 64000 - 1,
        .RepetitionCounter = 0,
        .Autoreload = 100 - 1,
    };
    LL_TIM_Init(TIM1, &tim_init);
    LL_TIM_SetClockSource(TIM1, LL_TIM_CLOCKSOURCE_INTERNAL);
    LL_TIM_EnableARRPreload(TIM1);

    LL_TIM_OC_InitTypeDef tim_oc_init = {
        .OCMode = LL_TIM_OCMODE_TOGGLE,
        .OCNPolarity = LL_TIM_OCPOLARITY_HIGH,
        .OCNState = LL_TIM_OCSTATE_ENABLE,
        .OCNIdleState = LL_TIM_OCIDLESTATE_LOW,
    };
    LL_TIM_OC_Init(TIM1, LL_TIM_CHANNEL_CH1, &tim_oc_init);
    LL_TIM_EnableAllOutputs(TIM1);
    LL_TIM_EnableCounter(TIM1);
    LL_TIM_EnableUpdateEvent(TIM1);
    LL_TIM_EnableDMAReq_UPDATE(TIM1);
    LL_TIM_GenerateEvent_UPDATE(TIM1);
}

PWM using DMA to update repetition counts and enabled output

In this example we use Direct Memory Access (DMA) CH1 to update the timer's RCR register (which is the RepetitionCounter). We configure DMA to read the word (32-bit) values from a buffer, incrementing the index, running in a circular mode (when it gets to the end it loops back to the start of the buffer). We write to the RCR register` and don't increment (so each write is to the Repetition counter). The RepetitionCounter will decrement every time the Autoreload count is met, but the DMA actions will only fire when the RepetitionCounter is 0 & the Autoreload count is met. This means the counter represents how many cycles to output.

We use DMA CH2 to update the timer's CCMR1 register (which is the state of the OCMode). We configure DMA to read the word value from a buffer, incrementing the index, running in a circular mode. The data read switches between FORCED_INACTIVE (so GPIO PIN is LOW) vs PWM1 mode (so GPIO PIN is PWM signal).

In the below sample we blink 2, 4 and 8 times with a duty cycle of 25% (100 of 400) and a frequency of 1000/400 = 2.5 Hz. We also have off for 2 cycles (800ms) between the blink counts.

uint32_t counts[] = {2, 2, 4, 2, 8, 2};

#define CCMR_OFF LL_TIM_OCMODE_FORCED_INACTIVE
#define CCMR_ON LL_TIM_OCMODE_PWM1

uint32_t enabled[] = {CCMR_OFF, CCMR_ON};

void pwm_dma_repeat_enabled() {
    LL_DMA_InitTypeDef dma_led_transition_timer = {
        .Direction = LL_DMA_DIRECTION_MEMORY_TO_PERIPH,
        .PeriphOrM2MSrcAddress = (uint32_t)&TIM1->RCR,
        .PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT,
        .PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_WORD,
        .MemoryOrM2MDstAddress = (uint32_t)counts,
        .MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT,
        .MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_WORD,
        .Mode = LL_DMA_MODE_CIRCULAR,
        .NbData = COUNT_OF(counts),
        .PeriphRequest = LL_DMAMUX_REQ_TIM1_UP,
        .Priority = LL_DMA_PRIORITY_HIGH,
    };
    LL_DMA_Init(DMA1, LL_DMA_CHANNEL_1, &dma_led_transition_timer);
    LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);

    LL_DMA_InitTypeDef dma_led_transition_enabled = {
        .Direction = LL_DMA_DIRECTION_MEMORY_TO_PERIPH,
        .PeriphOrM2MSrcAddress = (uint32_t)&TIM1->CCMR1,
        .PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT,
        .PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_WORD,
        .MemoryOrM2MDstAddress = (uint32_t)enabled,
        .MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT,
        .MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_WORD,
        .Mode = LL_DMA_MODE_CIRCULAR,
        .NbData = COUNT_OF(enabled),
        .PeriphRequest = LL_DMAMUX_REQ_TIM1_UP,
        .Priority = LL_DMA_PRIORITY_HIGH,
    };
    LL_DMA_Init(DMA1, LL_DMA_CHANNEL_2, &dma_led_transition_enabled);
    LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_2);

    furi_hal_gpio_init_ex(
        &gpio_ext_pa7, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedVeryHigh, GpioAltFn1TIM1);

    furi_hal_bus_enable(FuriHalBusTIM1);

    LL_TIM_InitTypeDef tim_init = {
        .CounterMode = LL_TIM_COUNTERMODE_UP,
        .ClockDivision = LL_TIM_CLOCKDIVISION_DIV1,
        .Prescaler = 64000 - 1,
        .RepetitionCounter = 0,
        .Autoreload = 400-1,
    };
    LL_TIM_Init(TIM1, &tim_init);
    LL_TIM_SetClockSource(TIM1, LL_TIM_CLOCKSOURCE_INTERNAL);
    LL_TIM_EnableARRPreload(TIM1);

    LL_TIM_OC_InitTypeDef tim_oc_init = {
        .OCMode = LL_TIM_OCMODE_FORCED_INACTIVE, // LL_TIM_OCMODE_PWM1,
        .CompareValue = 100-1,
        .OCNPolarity = LL_TIM_OCPOLARITY_HIGH,
        .OCNState = LL_TIM_OCSTATE_ENABLE,
        .OCNIdleState = LL_TIM_OCIDLESTATE_LOW,
    };
    LL_TIM_OC_Init(TIM1, LL_TIM_CHANNEL_CH1, &tim_oc_init);
    LL_TIM_OC_EnablePreload(TIM1, LL_TIM_CHANNEL_CH1); // TIM1_CCR1
    LL_TIM_EnableAllOutputs(TIM1);
    LL_TIM_EnableCounter(TIM1);
    LL_TIM_EnableUpdateEvent(TIM1);
    LL_TIM_EnableDMAReq_UPDATE(TIM1);
    LL_TIM_GenerateEvent_UPDATE(TIM1);
}
Clone this wiki locally