STM8S + SDCC: Программирование БЕЗ SPL. Cистема автопробуждения AWU, модуль генерации звуковой частоты BEEP, cторожевые оконный и независимый таймеры

разделы: STM8 , дата: 22 апреля 2018г.

Статья рассматривает вспомогательные таймеры и систему низкочастотного тактирования в микроконтроллерах STM8S. Упор делается на "чистом" программировании на Си и Ассемблере без использования сторонних библиотек.

Несмотря на то, что рассматриваются вспомогательные модули, на мой взгляд тема довольно сложная, в первую очередь из-за огромного количества "подводных камней".

Документация которая понадобится для прочтения статьи: Reference Manual STM8S - RM0016, главы: 12 (AWU), 13 (BEEP), 14 (Independet Watchdog), 15 (Window Watchdog). В качестве целевого микроконтроллера я буду использовать 20-пиновый STM8S103F3P6.

В статье используются формулы в формате MathML который поддерживается браузером Firefox, для браузеров Chrome и Opera потребуется установить одноименное расширение MathML.

Скачать полные исходники со сборочными файлам и скомпилированными прошивками, можно по ссылке в конце статьи.

    Содержание:
  1. Режимы энергосбережения;
  2. Описание системы автопробуждения;
  3. Blink на таймере AWU;
  4. Измерение частоты LSI с помощью захвата по таймеру TIM1;
  5. Тактирование AWU от внешнего кварца или генератора HSE;
  6. Генератор для пьезодинамика;
  7. Независимый сторожевой таймер Watchdog;
  8. Оконный сторожевой таймер;

1. Режимы энергосбережения

Микроконтроллеры STM8 имеют три инструкции для перехода в режим энергосбережения: wfi, wfe, halt. В активный режим они возвращаются при прерывании или event'у, если используется wfe.

Имеется два основных режима энергосбережения: wait и halt. При первом отключается от тактирования только CPU, при втором отключается главная шина тактирования fMASTER и соответственно подключенные к ней CPU и вся периферия. Напомню, что в wait и active режимах периферию можно отключать индивидуально.

Режим Active-Halt является вариантом Halt режима, и отличается от оного запущенным AWU таймером. Режим имеет два варианта: с включенным и выключенным регулятором напряжения MVR (см. описание регистра CLK_ICKR)

Все вместе это дает четыре режима энергосбережения:

2. Описание системы автопробуждения

Система автопробуждения микроконтроллеров STM8S - это таймер который запускается только после выполнении инструкции halt, т.е. перехода в спящий режим. Срабатывание прерывания AWU приводит к пробуждению микроконтроллера, его переходу в активный режим.

Регистры которые относятся к AWU и их предустановленные значения:

Здесь, AWU_CSR - это конфигурационный регистр, AWU_APR - хранит количество тактов, которое таймер пропускает перед срабатыванием, AWU_TBR - хранит коэффициент умножения AWU_APR.

Описание регистров AWU.

Конфигурационный регистр AWU_CSR имеет три флаговых бита:

Здесь, AWUF - флаг срабатывания прерывания, AWUEN - активирует таймер (но отчет начнется только после инструкции halt), MSR используется при калибровке LSI, когда тактовая частота LSI подается на вход таймера TIM1 или TIM3.

AWU_APR хранит 6-битное значение предделителя.

AWU_TBR хранит коэффициент умножения предделителя.

Если таймер не используется, то в AWU_TBR следует записать ноль.

Далее приводится таблица временных диапазонов, которые доступны при тех или иных значениях AWU_TBR:

Хочу обратить внимание на графу допустимых значений APRDIV (последний столбец). Значение APRDIV физически не может быть больше 64, т.к. это 6-битное значение. А вот иметь значение меньше 32 ничто не мешает. В данном случае это искусственное ограничение.

Примеры расчета значений AWU_APR и AWU_TBR:

    Пример первый.
  1. Заданное время 6мс;
  2. Частота таймера freqLS - 128 кГц.

Значение 6мс лежит в интервале 4мс - 8мс, таблицы выше. Этому интервалу соответствует значение AWU_TBR=0b101 и коэффициент - 24.

Тогда значение AWU_APR рассчитывается по следующей формуле:

6 м с = 2 4 * A P R D I V f r e q L S

Из чего следует:

A P R D I V = 6 * 1 0 -3 * f r e q L S 2 4 = 6 * 1 0 -3 * 128 * 1 0 3 2 4 = 6 * 128 16 = 48 = 0 x 30
    Пример второй.
  1. Заданное время 3с;
  2. Частота таймера freqLS - 128 кГц.

Значение 3с лежит в интервале 2.08с - 5.12с, таблицы выше. Этому интервалу соответствует значение AWU_TBR 0b1110 и коэффициент - 5 * 211.

Тогда значение AWU_APR рассчитывается по следующей формуле:

3 c = 5 * 2 11 * A P R D I V f r e q L S

Из чего следует:

A P R D I V = 3 * f r e q L S 5 * 2 11 = 3 * 2 7 * 1 0 3 5 * 2 11 = 300 8 = 37.5

В данном случае нужно будет округлить результат до целого значения: 37 или 38, и записать это значение в регистр AWU_APR.

3. Blink на таймере AWU

Сделаем простой пример мигалки, где задержка будет формироваться через таймер AWU. Пусть задержка будет продолжительностью в одну секунду. Тогда в AWU_TBR заносим число 12, а в AWU_APR - 62.

Текст программы получается таким:

#include <stdint.h>
#include "stm8s103f.h"

#define LED 5

void awu_irq(void) __interrupt(1) {
__asm
    bres 0x50f0,#5 ; Clear AWUF bit for AWU_CSR
__endasm;
}

int main( void )
{
    // Setup GPIO
    PB_DDR=(1<<LED);
    PB_CR1=(1<<LED);
    PB_ODR |= (1<<LED);

    AWU_TBR = 12;
    AWU_APR = 62;       // =1 sec
    AWU_CSR = 0x10;     // set AWUEN bit for AWUCSR

    for(;;)
    {
        PB_ODR |= (1<<LED);
        halt();
        PB_ODR &= ~(1<<LED);
        halt();
    }

}
    Этот простой пример был написан методом многих проб и ошибок, и отсюда следуют несколько интересных выводов:
  • AWU следует использовать обязательно с обработчиком прерывания AWU.
  • Для прерывания AWU не нужно разрешать прерывания инструкцией rim. Запрет на прерывания (инструкция sim) также не имеет никакого воздействия на AWU.
  • Значения бита AL глобального конфигурационного регистра тоже не имеет на AWU никакого воздействия.
  • Предварительно включать LSI не нужно.
  • В обработчике прерывания AWU нужно сбрасывать флаг AWUF регистра AWU_CSR обязательно битовой инструкцией bres. Инструкции: ld, mov - не работают корректно ( ПРОВЕРЬТЕ!).

Теперь, чтобы получить задержку большую чем это возможно по таблице AWU_TBR, например 10 секунд, можно сделать так:

#include <stdint.h>
#include "stm8s103f.h"

volatile uint8_t sec;
#define LED 5

void awu_irq(void) __interrupt(1) {
    sec++;
__asm
    bres 0x50f0,#5 ; Clear AWUF bit for AWU_CSR
__endasm;
}

int main( void )
{
    // Setup GPIO
    PB_DDR=(1<<LED);
    PB_CR1=(1<<LED);
    PB_ODR |= (1<<LED);

    AWU_TBR = 12;
    AWU_APR = 62;       // =1 sec
    AWU_CSR = 0x10;     // set AWUEN bit for AWUCSR

    for(;;)
    {
        sec=0;
        PB_ODR ^= (1<<LED);
        while (sec <10) halt();
    }

}

4. Измерение частоты LSI с помощью захвата по таймеру TIM1

Т.к. низкочастотный генератор LSI имеет погрешность ±12.5%, осуществлять с его помощью длительные задержки может оказаться довольно проблемной задачей.

Для ее решения возможно измерение фактической частоты LSI c помощью таймеров TIM1/TIM3. Подключение осуществляется с помощью флага MSR регистра AWU_CSR:

В stm8s103f3p6 нет таймера TIM3, поэтому остается только использовать TIM1. Мне бы не хотелось сейчас разбирать работу этого таймера в режиме захвата, в целом работает он аналогично таймерам в ATMega8. Т.е. таймер будет считать сколько его тиков прошло до появления сигнала на входе, после чего он заносит это значение в регистры результата. Если TIM1 будет работать на частоте 16 МГц, то при частоте LSI равной 128 кГц, должно получиться 125 тиков.

Однако есть пара моментов в TIM1, которые важны для понимания алгоритма измерения частоты LSI.

Основной конфигурационный регистр канала в режиме захвата: TIM1_CCMRx имеет поле предделителя ICxPSC который указывает количество импульсов для осуществления захвата:

Выставив значение ICxPSC в максимальное значение равное восьми, мы получим на выходе статистическое усредненное значение периода LSI. Что собственно нам и надо. Естественно, что значение периода вырастет в 8 раз (125 * 8 = 1000), что нужно будет учитывать при расчетах.

Второй нюанс заключается в том, что регистры результата захвата нельзя обнулить, они доступны только для чтения:

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

Теперь нужно будет решить, как из полученной величины рассчитать значение APRDIV. Величина которую мы получаем является периодом LSI умноженным на 8. Для частот LSI =128 кГц, и HSI/HSE =16 МГц она равняется 1000. Если величина меньше 1000, то частота LSI выше 128 кГц. Если больше 1000, то выше.

Допустим, что нужно рассчитать по измеренной величине периода LSI значение APRDIV для задержки в 1 секунду. Значение в одну секунду фактически находится на границе 12-го и 13-го диапазонов. Чтобы не перескакивать с одного на другой в зависимости от фактической частоты LSI, я предлагаю в качестве базы взять 13-й диапазон. Это избавит нас от ситуации когда значение APRDIV принимает значение больше 64.

Тогда расчетная формула будет такой:

A P R D I V = 1 * f r e q L S 2 12 = 1 2 12 * f r e q L S = 1 2 12 * 8 * 16 * 1 0 6 V a l u e

Где Value - это измеренное значение. Число 8 х 16 х 106 раскладывается на множители: 213 х 56. Тогда:

A P R D I V = 1 2 12 * 2 13 * 5 6 V a l u e = 2 1 * 5 6 V a l u e = 31250 V a l u e

В итоге, все сводится к простому делению.

Программа у меня получилась такой:

#include <stdint.h>
#include "stm8s103f.h"

#define bset(X,Y) __asm bset X,Y __endasm
#define LED 5

static uint8_t lsi_measurement();
volatile uint8_t sec;

void awu_irq(void) __interrupt(1) {
    sec++;
    bset(0x50f0,#5); // Clear AWUF bit for AWU_CSR
}


int main( void )
{
    // Set fCPU = 16MHz
    CLK_CKDIVR=0;
    // Setup GPIO
    PB_DDR=(1<<LED);
    PB_CR1=(1<<LED);
    PB_ODR |= (1<<LED);

    AWU_TBR = 13;
    AWU_APR = lsi_measurement();    // =1 sec
    AWU_CSR = 0x10;     // set AWUEN bit for AWUCSR

    for(;;)
    {
        sec=0;
        PB_ODR ^= (1<<LED);
        while (sec <10)  halt();
    }
}

Функцию lsi_measurement() я написал на ассемблере:

.module DELAY
.area CODE
.area HOME
.include "stm8s103f.s"

.globl _lsi_measurement
_lsi_measurement:
    ; LSI measurment
    bset AWU_CSR, #0        ; LSI measurement enable
    bres TIM1_CCER1,#0      ; capture disable
    mov TIM1_CCMR1, #0x0d   ; CC1 channel is configured as input, IC1 is mapped on TI1FP1; prescaller =8
    bres TIM1_CCER1, #1     ; Capture on a rising edge of TI1F or TI2F
    bset TIM1_CCER1,#0      ; capture enable
    bset TIM1_CR1,#0        ; turn on TIM1
    ; get first Value
wait_capture:
    btjf TIM1_SR1,#1, wait_capture
    pushw x
    ld a, TIM1_CCR1H
    ld (01,sp),a
    ld a, TIM1_CCR1L
    ld (02,sp),a
    bres TIM1_SR1,#1        ; Clear CC1 flag
    ; get second value
wait_capture_again:
    btjf TIM1_SR1,#1, wait_capture_again
    ldw y, TIM1_CCR1

    bres TIM1_SR1, #1       ; Clear CC1 flag
    bres TIM1_CCER1, #0     ; Capture disable
    bres TIM1_CR1, #0       ; turn off TIM1
    bres AWU_CSR, #0        ; LSI measurement disable

    subw y,(01,sp)          ; Value2 - Value1
    popw x
    tnzw y
    jrmi _lsi_measurement   ; if result is negative, try again

    ldw x, #31250
    divw x,y
    ld a,xl
    ret

5. Тактирование AWU от внешнего кварца или генератора HSE

Систему AWU возможно тактировать внешнего кварца или генератора HSE. Предделитель понижает частоту кварца до 128 кГц, при этом не обязательно fMASTER переключать на HSE. Хотя смысла в том, что бы использовать HSI при наличии кварца я не вижу.

Использовать можно кварцы на 16, 8 и 4 МГц. Включается все это хозяйство через Option Bytes, флаги CKAWUSEL и PRSC:

Тестовый пример:

#include <stdint.h>
#include "stm8s103f.h"

volatile uint8_t sec;
#define LED 5

void awu_irq(void) __interrupt(1) {
    sec++;
__asm
    bres 0x50f0,#5 ; Clear AWUF bit for AWU_CSR
__endasm;
}

int main( void )
{
    // Setup GPIO
    PB_DDR=(1<<LED);
    PB_CR1=(1<<LED);

    AWU_TBR = 12;
    AWU_APR = 62;       // =1 sec
    AWU_CSR = 0x10;     // set AWUEN bit for AWUCSR

    for(;;)
    {
        sec=0;
        PB_ODR ^= (1<<LED);
        while (sec <10) halt();
    }

}

6. Генератор для пьезодинамика

Модуль бипера также тактируется от низкочастотного LSI. Он имеет всего один конфигурационный регистр BEEP_CSR:

Пример программы:

#include <stdint.h>
#include "stm8s103f.h"

#define bset(X,Y) __asm bset X,Y __endasm
#define LED 5

volatile uint8_t sec;

void awu_irq(void) __interrupt(1) {
    sec++;
    bset(0x50f0,#5);        // Clear AWUF bit for AWU_CSR
}

int main( void )
{
    // Setup GPIO
    PB_DDR=(1<<LED);
    PB_CR1=(1<<LED);

    AWU_TBR = 12;
    AWU_APR = 62;       // =1 sec
    AWU_CSR = 0x10;     // set AWUEN bit for AWUCSR

//  BEEP_CSR=0x9e;      // peeeeeck... (high tone)
    BEEP_CSR=0x1e;      // buuuuuuuuuzzzzz (low tone)
    bset(0x50f3,#5);    // Enable BEEP
    for(;;)
    {
        sec=0;
        PB_ODR ^= (1<<LED);
        while (sec <10) halt();
    }

}

7. Независимый сторожевой таймер Watchdog

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

Интервал сторожевого таймера можно настроить в пределах от нескольких микросекунд до одной секунды. Т.к. сторожевой таймер тактируется от LSI, на все интервалы должна распространяться погрешность генератора ±12.5%.

Сторожевой таймер имеет три регистра и систему защиты от случайного изменения при дребезге питания или при непредсказуемом поведении (undefined behaviour).

Функционально устройство сторожевого таймера показано на следующей схеме:

Интервал сторожевого таймера рассчитывается по формуле:

Сторожевой таймер имеет три регистра: регистр доступа - IWDG_KR, регистр предделителя - IWDG_PR, и регистр счетчика - IWDG_RLR

Прежде чем что-то записать в последние два регистра, сначала следует открыть к ним доступ записью числа 0x55 в регистр IWDG_KR. При записи в IWDG_KR числа 0xAA, закрывается доступ на запись регистров IWDG_PR и IWDG_RLR, их содержимое при этом обновляется. Если при закрытом доступе к IWDG_PR и IWDG_RLR записать в IWDG_KR число 0xAA, содержимое регистра счетчика - IWDG_RLR восстановится.

Описание регистра доступа IWDG_KR:

Регистр предделителя IWDG_PR:

Счетчик IWDG_RLR:

Пример программы:

#include <stdint.h>
#include "stm8s103f.h"

#define bset(X,Y) __asm bset X,Y __endasm
#define bres(X,Y) __asm bres X,Y __endasm
#define LED 5

extern void delay(uint16_t value);

int main( void ) {
    // Setup GPIO
    PB_DDR=(1<<LED);
    PB_CR1=(1<<LED);
    // Setup Buzzer
    BEEP_CSR=0x1e;      // buuuuuuuuuzzzzz (low tone)
    bset(0x50f3,#5);    // Enable BEEP
    delay(100);
    bres(0x50f3,#5);
    // Watchdog Setup 
    IWDG_KR=0xcc;
    IWDG_KR=0x55;       // unlock IWDG_PR & IWDG_RLR
    IWDG_PR=0x6;        // =256
    IWDG_RLR=0xff;      // 1.024 sec
    IWDG_KR=0xaa;       // lock IWDG_PR & IWDG_RLR

    // main loop
    for(;;)
    {
        PB_ODR |= (1<<LED);
        delay(500);
        IWDG_KR=0xaa;
        delay(500);
        IWDG_KR=0xaa;
        PB_ODR &= ~(1<<LED);
        delay(500);
        IWDG_KR=0xaa;
        delay(500);
        IWDG_KR=0xaa;
        delay(800);
        IWDG_KR=0xaa;
        delay(500);
        IWDG_KR=0xaa;

    }
}

Здесь при включении микроконтроллера пьезодинамик издает короткий сигнал, а мигающий светодиод имитирует рабочий цикл. Если в аргументах функции delay() поставить аргумент больше 1000, то пьезодинамик начнет постоянно пищать.

8. Оконный сторожевой таймер

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

ВНИМАНИЕ! Я должен предупредить, что на моем STM8S103F3P6 этот таймер работал немого не так как описано в документации. Это означает, что на другом микроконтроллере или ревизии его поведение возможно будет более "каноническим".

Ок. Таймер Имеет два регистра: WWDG_CR и WWDG_WR:

Таймер тактируется от высокочастотных HSI/HSE с фиксированным предделителем равным 12288. При этом, счетчик таймера 5-битный, .т.е. диапазон его значений: 0-63.

Таймер продолжает работать после инструкции wfi и останавливается после halt. Через Option Bytes можно задать чтобы после halt микроконтроллер перезагружался.

Счетчик таймера задается в конфигурационном регистре WWDG_CR, биты 0-5. Старший, 7-й бит управляет состоянием таймера: включено/выключено. Сброс 6-го бита регистра при включенном таймере приводит к перезагрузке микроконтроллера. Счетчик вычитающий.

Оконный регистр WWDG_WR задает минимальное время работы таймера. Если счетчик WWDG_CR был обновлен при значении большем чем содержится в WWDG_WG, микроконтроллер будет перезагружен.

Следующий рисунок демонстрирует задание временного окна сторожевого таймера:

Далее показаны минимальные и максимальные временные интервалы которые можно получить при частотах HSI равных 2 и 16 МГц:

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

Допустим нужно задать временную границу таймаута, после которого ждать уже бессмысленно, и следует осуществить перезагрузку микроконтроллера:

#include <stdint.h>
#include "stm8s103f.h"

#define bset(X,Y) __asm bset X,Y __endasm
#define bres(X,Y) __asm bres X,Y __endasm
#define LED 5

extern void delay(uint16_t value);

int main( void ) {
    // Setup GPIO
    PB_DDR=(1<<LED);
    PB_CR1=(1<<LED);
    PB_ODR |=(1<<LED);
    // Setup Buzzer
    BEEP_CSR=0x1e;      // buuuuuuuuuzzzzz (low tone)
    bset(0x50f3,#5);    // Enable BEEP
    delay(100);
    bres(0x50f3,#5);
    // Watchdog Setup 
    WWDG_WR=0x40;       // max value
    WWDG_CR=0x7f;       // max value
    bset(0x50d1,#7);    // start window watchdog

    // main loop
    for(;;)
    {
        PB_ODR ^= (1<<LED);
        delay(310);
        WWDG_CR|=0x7f;
    }
}

Здесь, если в главном цикле указать задержку delay(350) или более, то микроконтроллер уже начнет перезагружаться. Задержку можно варьировать от 1 до примерно 310(подбирается экспериментально). Частота HSI - 2 МГц.

Вначале программы счетчику присваивается максимальное значение 0x7f. В процессе работы он уменьшается, и если достигает значения 0x3f, то микроконтроллер перезагружается.

Теперь, допустим, нужно сделать контроль минимального времени задержки. Если в первом примере закоментировать строку delay(310), и перепрошить микроконтроллер, пьезодинамик начнет непрерывно пищать.

И тут на помощь приходит оконный регистр. Если выставить WWDG_WR в значение 0x7f, писк прекратиться. Оконный таймер перестанет реагировать на минимальный интервал. Если WWDG_WR выставить в значение равное 0x7e, писк снова возобновится.

В следующем примере, если выставить границу цикла не в 100, а скажем в 90 или меньше, пьезодинамик начнет пищать:

#include <stdint.h>
#include "stm8s103f.h"

#define bset(X,Y) __asm bset X,Y __endasm
#define bres(X,Y) __asm bres X,Y __endasm
#define LED 5

extern void delay(uint16_t value);

int main( void ) {
    // Setup GPIO
    PB_DDR=(1<<LED);
    PB_CR1=(1<<LED);
    PB_ODR |=(1<<LED);
    // Setup Buzzer
    BEEP_CSR=0x1e;      // buuuuuuuuuzzzzz (low tone)
    bset(0x50f3,#5);    // Enable BEEP
    delay(100);
    bres(0x50f3,#5);
    // Watchdog Setup 
    WWDG_WR=0x7e;       // min value
    WWDG_CR=0x7f;       // max value
    bset(0x50d1,#7);    // start window watchdog

    // main loop
    for(;;)
    {
        uint16_t i;
        PB_ODR ^= (1<<LED);
        for(i=0;i<0x100;i++);
        WWDG_CR|=0x7f;
    }
}

Данный пример подходит для выставления минимального времени срабатывания оконного таймера, но как "двигать" эту границу я не знаю. Фактически, это можно только включить или отключить. Значение 0x7e регистра WWDG_WR, можно поменять на любое меньшее значения регистра WWDG_CR.

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

Если ждать 300 мс нет необходимости, то максимальное время задержки можно сократить до 4 мс:

#include <stdint.h>
#include "stm8s103f.h"

#define bset(X,Y) __asm bset X,Y __endasm
#define bres(X,Y) __asm bres X,Y __endasm
#define LED 5

extern void delay(uint16_t value);

int main( void ) {
    // Setup GPIO
    PB_DDR=(1<<LED);
    PB_CR1=(1<<LED);
    PB_ODR |=(1<<LED);
    // Setup Buzzer
    BEEP_CSR=0x1e;      // buuuuuuuuuzzzzz (low tone)
    bset(0x50f3,#5);    // Enable BEEP
    delay(100);
    bres(0x50f3,#5);
    // Watchdog Setup 
    WWDG_WR=0x3f;       // min value
    WWDG_CR=0x40;       // max value
    bset(0x50d1,#7);    // start window watchdog

    // main loop
    for(;;)
    {
        uint16_t i;
        PB_ODR ^= (1<<LED);
//      for(i=0;i<0x100;i++);
        delay(4);
        WWDG_CR|=0x40;
    }
}

Здесь, если значение delay() выставить больше 4-х, то микроконтроллер будет перезагружаться по срабатыванию сторожевого таймера.

В завершении напомню, что при изменении частоты fMASTER, соответственно поменяются и тайминги.

Скачать полные исходники со сборочными файлам и скомпилированными прошивками, можно по этой ссылке.

поделиться: