Skip to content

Instantly share code, notes, and snippets.

@stecman
Last active January 7, 2024 01:44
Show Gist options
  • Save stecman/f748abea0332be1e41640fd25b5ca861 to your computer and use it in GitHub Desktop.
Save stecman/f748abea0332be1e41640fd25b5ca861 to your computer and use it in GitHub Desktop.
STM8S code examples

This is a collection of code snippets for various features on the STM8S family microcontrollers (specifically the STM8S003F3). These are written against the STM8S/A SPL headers and compiled using SDCC.

Some of this controller's functions aren't particularly intuitive to program, so I'm dumping samples for future reference here. These are based on the STM8S documentation:

Run at 16MHz

// Configure the clock for maximum speed on the 16MHz HSI oscillator
// At startup the clock output is divided by 8
CLK->CKDIVR = 0x0;

PWM output using TIM1

Note that on the STM8S003F3, TIM1_CH1 output requires configuring an Alternate Pin Function (see next snippet).

const uint16_t tim1_prescaler = 0;
TIM1->PSCRH = (tim1_prescaler >> 8);
TIM1->PSCRL = (tim1_prescaler & 0xFF);

const uint16_t tim1_auto_reload = 16000; // 1KHz assuming at 16MHz clock
TIM1->ARRH = (tim1_auto_reload >> 8);
TIM1->ARRL = (tim1_auto_reload & 0xFF);

const uint16_t tim1_compare_reg1 = 4000; // 25% duty cycle
TIM1->CCR1H = (tim1_compare_reg1 >> 8);
TIM1->CCR1L = (tim1_compare_reg1 & 0xFF);

// Set up compare channel 1
TIM1->CCER1 = TIM1_CCER1_CC1E; // Enable compare channel 1 output
TIM1->CCMR1 = TIM1_OCMODE_PWM1; // Make OC1REF high when counter is less than CCR1 and low when higher

TIM1->EGR |= TIM1_EGR_UG; // Generate an update event to register new settings
TIM1->BKR = TIM1_BKR_MOE; // Enable TIM1 output channels
TIM1->CR1 = TIM1_CR1_CEN; // Enable the counter

Using alternate pin functions (APF)

Some pins on the STM8S003 series are marked as having an "alternate function after remap". These are only documented in the datasheet, with register description under Option bytes.

As option bytes are stored in eeprom, they're a little more involved to set than regular registers. Eeprom can be addressed directly for reading, but needs to be unlocked before it can be written.

Option bytes are intended to be programmed once when uploading code to the chip, but they can be changed "on the fly" as the datasheet mentions:

// Set a bit in the Alternate Function Register (AFR) to switch pin-mapping
// This is stored in flash and is not a directly writable register
{
    const uint32_t AFR_DATA_ADDRESS = 0x4803;
    const uint32_t AFR_REQUIRED_VALUE = 0x1; // Enable TIM1_CH1 pin

    const uint16_t valueAndComplement = FLASH_ReadOptionByte(AFR_DATA_ADDRESS);
    const uint8_t currentValue = valueAndComplement >> 8;

    // Update value if the current option byte is corrupt or the wrong value
    if (valueAndComplement == 0 || currentValue != AFR_REQUIRED_VALUE) {
        FLASH_Unlock(FLASH_MEMTYPE_DATA);
        FLASH_ProgramOptionByte(AFR_DATA_ADDRESS, AFR_REQUIRED_VALUE);
        FLASH_Lock(FLASH_MEMTYPE_DATA);
    }
}

This works in a pinch and avoids an extra programming step, but switching function repeatedly during run-time is probably a bad idea:

  • There's likely an endurance issue as each change is written to flash/eeprom (datasheet claims 100k writes).

  • Writing some of the option bytes requires two writes: one for the data and one for its complement. If a reset occurs between the two writes, the option bytes need to be reprogrammed before the SWIM interface can program the chip again (stm8flash and ST Visual Programmer can still program the option bytes in this state, but error when trying to program the main flash).

Blocking ADC read

// Set up ADC
ADC1->CSR |= ADC1_CHANNEL_4; // Select channel to read
ADC1->CR2 = ADC1_CR2_ALIGN; // Place LSB in lower register
ADC1->CR1 = ADC1_CR1_ADON; // Power on the ADC

// Trigger single conversion
// (Yes, you write a 1 to the register a second time to do this - super intuitive...)
ADC1->CR1 |= ADC1_CR1_ADON;

// Wait until conversion is done
while ((ADC1->CSR & ADC1_CSR_EOC) == 0);

// Clear done flag
ADC1->CSR &= ~ADC1_CSR_EOC;

// Load ADC reading (least-significant byte must be read first)
uint16_t result = ADC1->DRL;
result |= (ADC1->DRH << 8);

ADC conversions driven by TIM1

This uses TIM1's compare channel 1 to trigger ADC readings. This can be slightly simpler if TRGO is configured to trigger on timer reset instead.

void init_adc(void)
{
    // Configure ADC to do conversion on timer 1's TRGO event
    ADC1->CSR = ADC1_CSR_EOCIE | // Enable interrupt at end of conversion
                ADC1_CHANNEL_4; // Convert on ADC channel 4 (pin D3)

    ADC1->CR2 = ADC1_CR2_ALIGN | // Place LSB in lower register
                ADC1_CR2_EXTTRIG; // Start conversion on external event (TIM1 TRGO event)

    ADC1->CR1 = ADC1_PRESSEL_FCPU_D18 | // ADC @ fcpu/18
                ADC1_CR1_ADON; // Power on the ADC


    // Configure TIM1 to trigger ADC conversion automatically
    const uint16_t tim1_prescaler = 16000; // Prescale the 16MHz system clock to a 1ms tick
    TIM1->PSCRH = (tim1_prescaler >> 8);
    TIM1->PSCRL = (tim1_prescaler & 0xFF);

    const uint16_t tim1_auto_reload = 69; // Number of milliseconds to count to
    TIM1->ARRH = (tim1_auto_reload >> 8);
    TIM1->ARRL = (tim1_auto_reload & 0xFF);

    const uint16_t tim1_compare_reg1 = 1; // Create a 1ms OC1REF pulse (PWM1 mode)
    TIM1->CCR1H = (tim1_compare_reg1 >> 8);
    TIM1->CCR1L = (tim1_compare_reg1 & 0xFF);

    // Use capture-compare channel 1 to trigger ADC conversions
    // This doesn't have any affect on pin outputs as TIM1_CCER1_CC1E and TIM1_BKR_MOE are not set
    TIM1->CCMR1 = TIM1_OCMODE_PWM1; // Make OC1REF high when counter is less than CCR1 and low when higher
    TIM1->EGR = TIM1_EGR_CC1G; // Enable compare register 1 event
    TIM1->CR2 = TIM1_TRGOSOURCE_OC1REF; // Enable TRGO event on compare match

    TIM1->EGR |= TIM1_EGR_UG; // Generate an update event to register new settings

    TIM1->CR1 = TIM1_CR1_CEN; // Enable the counter
}

void adc_conversion_irq(void) __interrupt(ITC_IRQ_ADC1)
{
    // Clear the end of conversion bit so this interrupt can fire again
    ADC1->CSR &= ~ADC1_CSR_EOC;

    // Load ADC reading (least-significant byte must be read first)
    uint16_t result = ADC1->DRL;
    result |= (ADC1->DRH << 8);

    // Use result ...
}

Crude delay function

These assume the clock is running at 16MHz and ok for crude blocking timing.

void _delay_us(uint16_t microseconds) {
    TIM4->PSCR = TIM4_PRESCALER_1; // Set prescaler

    // Set count to approximately 1uS (clock/microseconds in 1 second)
    // The -1 adjusts for other runtime costs of this function
    TIM4->ARR = ((16000000L)/1000000) - 1;


    TIM4->CR1 = TIM4_CR1_CEN; // Enable counter

    for (; microseconds > 1; --microseconds) {
        while ((TIM4->SR1 & TIM4_SR1_UIF) == 0);

        // Clear overflow flag
        TIM4->SR1 &= ~TIM4_SR1_UIF;
    }
}

void _delay_ms(uint16_t milliseconds)
{
    while (milliseconds)
    {
        _delay_us(1000);
        milliseconds--;
    }
}

Pin change interrupt

void init(void)
{
    // Configure pin change interrupt on pin B4
    EXTI->CR1 |= 0x04; // Rising edge triggers interrupt
    GPIOB->DDR &= ~(1<<4); // Input mode
    GPIOB->CR1 |= (1<<4);  // Enable internal pull-up
    GPIOB->CR2 |= (1<<4);  // Interrupt enabled
}

void portb_pin_change_irq(void) __interrupt(ITC_IRQ_PORTB)
{
    // Do something...
}
@atochukwu0
Copy link

Please @stecman, can you show example of ADC2 trigger and scan mode, measuring 5 ADC channels . ADC2 . Please .

@wuschel-brompf
Copy link

wuschel-brompf commented Jul 13, 2021

awesome - the first person telling the source of their wisdom. Your snippets got me started, but the linked reference manual was exactly what I needed. Now that I have RM0016 (thanks to you), I see that it is referenced to in all product datasheets facepalm.

Thank you!

@stecman
Copy link
Author

stecman commented Aug 21, 2021

ADC2 trigger and scan mode, measuring 5 ADC channels

@atochukwu0 page 428 in the the reference manual lists "scan mode for single and continuous conversion" as a feature only available on ADC1. ADC2 is simpler and lacks the output buffer that allows for ADC1's scan mode. To achieve the scan behaviour on a part that only has ADC2, you'd need to implement it in software.

What part are you using, out of curiosity? None of the STM8 parts I have on hand have ADC2, so I can't test any example code for this case.

@atochukwu0
Copy link

L

ADC2 trigger and scan mode, measuring 5 ADC channels

@atochukwu0 page 428 in the the reference manual lists "scan mode for single and continuous conversion" as a feature only available on ADC1. ADC2 is simpler and lacks the output buffer that allows for ADC1's scan mode. To achieve the scan behaviour on a part that only has ADC2, you'd need to implement it in software.

What part are you using, out of curiosity? None of the STM8 parts I have on hand have ADC2, so I can't test any example code for this case.

Am using STM8S207K8

@Benedito821
Copy link

Benedito821 commented Jul 3, 2023

This is a really good note . One of these particularities made me spend too much time to move forward on some projects. Regarding to the option bytes, a handier way of managing them (for those who use the IAR IDE) is to downloading the code, running the debugging , going to the menu ST_LINK, then option bytes... . A window is going to pop up and we can chose the needed alternate function, right-mouse click it and switch to Alternate active (for option bytes AFR0-AFR7).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment