Режимы энергосбережения микроконтроллеров STM8Lх51: Low Power Run Mode и Low Power Wait Mode

разделы: STM8 , дата: 06 ноября 2022г.

Данная статья посвящена ультраэкономичным режимам энергосбережения микроконтроллеров STM8L: "Low Power Run Mode" и "Low Power Wait Mode". Данные режимы характерны тем, что в них отключатся флеш-память, а весь код выполняется из оперативной памяти. Также отключается главный регулятор напряжения MVR, а тактирование осуществлется от низкочастотного генератора на 38 кГц или от часового кварца. За счет этого удается добится кардинального снижения энергопотребления микроконтроллера до 3-5 мкА.

На мой взгляд, данная технология ST привлекательна за счет того, что в отличии от большинства других режимов энергосбережения, микроконтроллер не находится в режиме сна подавляющую часть времени, а продолжает свою работу, пусть и на невысокой частоте, тактируясь от LSE.

В качестве примера для данных режимов энергосбережения я бы хотел рассмотреть создание часов на батарейном питании. Пусть будет две пальчиковые батареи с ресурсом работы в один год. В качестве индикатора будет использоваться шести-разрядный жидко-кристаллический индикатор (ЖКИ) на контроллере HT1621. Данный индикатор недорогой (около 200р), и он свободно продается на али.

На первый взгляд задача может показаться примитивной, но с учетом необходимости работы в режиме пониженного энергопотребления все становится не так просто. Отключая флеш-память, вы остаетесь без прерываний вообще, т.к. в stm8 таблица прерываний не может менять местоположение, она всегда располагается на флеш-памяти. В качестве альтернативы прерываниям существует механизм Event'ов, которого нет в S-линейке микроконтроллеров STM8. Кроме того, требуется рабочий цикл программы вместе с используемыми подпрограммами и функциями размещать в оперативной памяти, размер которой в микроконтроллере STM8L051F3 равен всего одному килобайту. А кроме кода программы, там еще располагаются глобальные переменные и стек. И вот так с ходу написать такой проект мало у кого получится, прежде придется порядочно посидеть над Reference Manual. А в проекте, еще используются недокументированные функции.

    В целом, статья делится условно на три части:
  1. Часть посвященна ЖКИ индикатору
  2. Часть посвященная режимам энергосбережения STM8L: "Low Power Run Mode" и "Low Power Wait Mode"
  3. Часть посвященная программированию часов
    Требуемая аппаратура:
  1. Для часов нам понадобится: а) ЖКИ индикатор, б) микроконтроллер STM8L051F3P6, в) энкодер с кнопкой, г) две батарейки АА или ААА с держателем, д) макетка.
  2. Т.к. микроконтроллер STM8L051F3P6 можно прошить не более чем 100 раз, для разработки нам понадобится микроконтроллер STM8L151xx или STM8L152xx. Сгодится так же STM8L-DISCOVERY. Ещё потребуется ST-Link и компьютер. Для оценки энергопотребления я буду пользоваться платой MSP430FR2433 с инструментом EnergyTrace, вы же можете пользоваться обычным мультиметром.

Из софта нам понадобится компилятор Cosmic и среда разработки STVD, т.к. у SDCC нет инструментов загрузки кода в оперативную память и компиляции кода без абсолютных адресов, т.е. строго с относительной адресацией.

Полезные материалы:

  1. Статья по компилятору Cosmic: "STM8S105 + COSMIC: Запись в EEPROM и FLASH память микроконтроллера"
  2. Даташит на HT1621: "https://www.radiokot.ru/artfiles/6611/01.pdf"
  3. Reference Manual STM8L: "RM0031"
  4. Даташит на микроконтроллер STM8L051F3: "https://www.st.com/resource/en/datasheet/stm8l051f3.pdf"
  5. Даташит на микроконтроллер STM8L151xx: "https://www.st.com/resource/en/datasheet/stm8l151c8.pdf"
  6. Топик на easyelectronics.ru: "STM8L encoder mode"

Содержание:

I. ЖК дисплей на контроллере HT1621

  1. Обзор ЖКИ на HT1621
  2. Пишем драйвер дисплея HT1621 для STM8S103F3

II. Работа с микроконтроллером STM8L151C8 в среде STVD+Cosmic

  1. Структура базового проекта с мигающим светодиодом
  2. Использование Wait режима энергосбережения
  3. Подключение дисплея HT1621 к микроконтроллеру STM8L151C8 и оценка энергопотребления
  4. Добавляем энкодер на таймере TIM2
  5. Программа часов

III. Энергосберегающие режимы "Low Power Run Mode" и "Low Power Wait Mode"

  1. Механизм Event'ов и режим энергосбережения флеш-памяти IDDQ
  2. Переходим в режим Low Power Wait Mode с тактированием от LSI
  3. Переключение на тактирование от LSE
  4. Часы на микроконтроллере STM8L151C8
  5. Адаптация кода на микроконтроллер STM8L051F3
  6. Вместо заключения
На портал GitLab я выложил workspace для STVD со всеми примерами: https://gitlab.com/flank1er/stm8l_ht1621_clock
Так же репозиторий включает драйвер дисплея ht1621 для stm8s103f3 написаный на SDCC

1) Обзор ЖКИ на HT1621

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

Потребление тока на данный модуль заявляется следующее (из описаний с али): с подсветкой 4 мА, без подсветки 0.4 мА. И это вторая наша проблема, т.к. 0.4 мА это очень, очень много. Я предположил, что эти цифры были получены при напряжении питания 5 Вольт, и при более низком напряжении, потребление упадет.

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

Модуль имеет контактную площадку с выводами: Vdd (питание дисплея), VddLED(питание подсветки), GND, CS(chip select), WR (тактирование). Контроллер HT1621 работает на SPI протоколе, он имеет раздельное тактирование для записи (WR) и для чтения (RD). Поэтому тактовый сигнал обозначается WR.

Про контроллер HT1621 можно почитать в даташите https://www.radiokot.ru/artfiles/6611/01.pdf.

Без подсветки дисплей выглядит как-то так:

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

На ютубе я нашел канал, где сравниваются дисплеи разных часов, и видно, что подожка дисплея на часах слева более темная, а на часах справа более светлая. За счет этого, часы справа выглядят более привлекательно. И человек в ролике прокомментировал это так, что мол в дисплее на часах справа использовались более дорогие и качественные материалы.

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

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

Здесь красным обведены две контактные площадки "S1". При покупке, верхняя площадка была запаяна, а нижняя распаяна. В этом положении, подсветка всегда включена и питается от Vdd контакта. Соответственно, чтобы управлять подсветкой через VddLED, я распаял верхнюю площадку и запаял нижнюю, как на фото.

Второй момент - это резистор R1, который управляет контрастом дисплея. Т.к. модуль рассчитан на пять вольт, при питании от 3.3 Вольта контрастность дисплея очень низкая. Я припаял параллельно подстроечный резистор, и выкрутил на общее значение сопротивления в 2 кОм, т.к. контрастность дисплея зависит от питающего напряжения.

Ячейка памяти контроллера HT1621 имеет 4 бита. Одна цифра дисплея состоит из 8 сегментов, включая точку (точка - это старший, т.е 7-й бит). Следовательно на один цифру дисплея уходит 2 ячейки памяти. В случае нашего дисплея требуется 6*2 =12 ячеек памяти. Всего контроллер HT1621 имеет 32 ячейки памяти. Т.е. он может работать теоретически, с 32*4 =128-сегментными ЖКИ.

Для проверки дисплея можно воспользоваться Arduino. Для этого достаточно будет установить библиотеку "ht1621":

Выбрать из примеров "hello world", и подключить дисплей согласно предложенной там распиновке:

  // cs to pin 13
  // wr to pin 12
  // Data to pin 8
  // backlight to pin 10

Питание можно использовать пять вольт. При питании 3.3 вольта дисплей будет довольно блеклым, если резистор R1 вы еще не трогали.

После прошивки дисплей должен заработать сразу, там пойдет таймер с миллисекундами.

2) Пишем драйвер дисплея HT1621 для STM8S103F3

По упомянутому драйверу HT1621 для Arduino, нетрудно восстановить алгоритм работы с дисплеем, даже не заглядывая в datasheet.

Для начала,чтобы проверить алгоритм, я написал на SDCC пробный драйвер для синей платы с микроконтроллером stm8s103f3. Полный код драйвера можно посмотреть здесь:

https://gitlab.com/flank1er/stm8l_ht1621_clock/-/tree/main/00_stm8s103f3_sdcc_driver

Я взял обычный "Blink" и добавил модуль "ht1621.c", в который начал добавлять одну за другой функции для работы с HT1621.

На нижнем уровне у нас идет функция передачи по SPI протоколу. Здесь у нас переменное число бит для передачи, поэтому будем использовать программную реализацию SPI:

void ht1621_send(uint8_t data, uint8_t len) {
    for(uint8_t i=0;i<len;i++) {
        PC_ODR=(data & 0x80) ? PC_ODR | (1<<DATA) : PC_ODR & ~(1<<DATA);
        PC_ODR &= ~(1<<WR);
        PC_ODR |= (1<<WR);
        data=(data<<1);
    }
}

Где, WR, CS и DATA это именованные константы:

#define  WR    5                    // Clock signal
#define  CS    4                    // [C]hip [S]elect
#define  DATA  6                    // MOSI

Далее пишем функцию для передачи команды:

void ht1621_send_cmd(uint8_t cmd) {
    PC_ODR &= ~(1<<CS);
    ht1621_send(0x80,4);
    ht1621_send(cmd,8);
    PC_ODR |=  (1<<CS);
}

Здесь, вначале, мы передаем число 0x80, а вернее 100b, которе говорит HT1621, что далее будет передана команда. Число 100b в руководстве называется mode (режимом). Всего имеется четыре режима:

Теперь мы можем написать функцию инициализации дисплея:

void ht1621_init() {
    ht1621_send_cmd(BIAS);
    ht1621_send_cmd(RC256);
    ht1621_send_cmd(SYSDIS);
    ht1621_send_cmd(WDTDIS1);
    ht1621_send_cmd(SYSEN);
    ht1621_send_cmd(LCDON);
}

Где именованные константы имеют следующие значения:

#define  BIAS     0x52             //0b1000 0101 0010  1/3duty 4com
#define  SYSDIS   0X00             //0b1000 0000 0000  关振系统荡器和LCD偏压发生器
#define  SYSEN    0X02             //0b1000 0000 0010 打开系统振荡器
#define  LCDOFF   0X04             //0b1000 0000 0100  关LCD偏压
#define  LCDON    0X06             //0b1000 0000 0110  打开LCD偏压
#define  XTAL     0x28             //0b1000 0010 1000 外部接时钟
#define  RC256    0X30             //0b1000 0011 0000  内部时钟
#define  TONEON   0X12             //0b1000 0001 0010  打开声音输出
#define  TONEOFF  0X10             //0b1000 0001 0000 关闭声音输出
#define  WDTDIS1  0X0A             //0b1000 0000 1010  禁止看门狗
#define  BUFFERSIZE 12

Аналогичным образом пишем функцию для передачи данных:

void ht1621_send_data(uint8_t adr, uint8_t value) {
    adr <<=2;
    PC_ODR &= ~(1<<CS);
    ht1621_send(0xa0, 3);
    ht1621_send(adr, 6);
    ht1621_send(value, 8);
    PC_ODR |=  (1<<CS);
}

Здесь вначале пишется режим (101b), затем шестибитный адрес, затем передаются данные. А вот с данными здесь не все так просто. На один адрес у нас приходится четыре бита данных, а здесь передается восемь. И в данном случае используется непрерывный режим передачи данных, при котором вначале передается адрес, а затем передаются данные. При этом счетчик адреса в HT1621 автоматически переходит на следующие ячейки памяти по мере поступления новых данных (автоинкремент).

В руководстве имеется датаграмма данного режима передачи:

Теперь нам нужно выяснить какие комбинации битов отвечают за какие сегменты дисплея. К счастью, данную работу проделали за нас, и в Arduino-библиотеке HT1621 имелась функция в которой уже было все записано. Там длинный switсh-оператор, и я его засунул под спойлер:

uint8_t ht1621_char_to_seg_bits(uint8_t ch) {
    switch (ch) {
    case '*': // For degree for now
        return 0b0110011;
    case '|':
        return 0b0000101;
    case '-':
        return 0b0000010;
    case '_':
        return 0b0001000;
    case '0':
        return 0b1111101;
    case '1':
        return 0b1100000;
    case '2':
        return 0b111110;
    case '3':
        return 0b1111010;
    case '4':
        return 0b1100011;
    case '5':
        return 0b1011011;
    case '6':
        return 0b1011111;
    case '7':
        return 0b1110000;
    case '8':
        return 0b1111111;
    case '9':
        return 0b1111011;
     case 'A':
    case 'a':
        return 0b1110111;
    case 'b':
    case 'B':
        return 0b1001111;
    case 'c':
    //  return 0b0001110;
    case 'C':
        return 0b0011101;
    case 'd':
    case 'D':
        return 0b1101110;
    case 'e':
    //  return 0b0001110;
    case 'E':
        return 0b0011111;
    case 'f':
    //  return 0b0000111;
    case 'F':
        return 0b0010111;
    case 'G':
    case 'g':
        return 0b1011101;
    case 'h':
    //  return 0b1000111;
    case 'H':
        return 0b1100111;
    case 'i':
    //  return 0b1000000;
    case 'I':
        return 0b1100000;
    case 'J':
    case 'j':
        return 0b1101000;
    case 'l':
    //  return 0b1100000;
    case 'L':
        return 0b0001101;
    case 'm':
    case 'M':
        return 0b1010100;
    case 'n':
    case 'N':
        return 0b1000110;
    case 'o':
        return 0b1001110;
    case 'P':
    case 'p':
        return 0b0110111;
    case 'q':
    case 'Q':
        return 0b1110011;
    case 'r':
    case 'R':
        return 0b0000110;
    case 'S':
    case 's':
        return 0b1011011;
    case 't':
    case 'T':
        return 0b0001111;
    case 'u':
    //  return 0b1001100;
    case 'U':
        return 0b1101101;
    case 'Y':
    case 'y':
        return 0b1101011;
    case 'z':
    case 'Z':
        return 0b0111110;
    case ' ':
    default:
        return 0b0000000;
    }
}

Лично я такое использование switch-операторов не люблю, поэтому использую массивы:

const uint8_t digits[10] = {0x7d,0x60,0x3e,0x7a,0x63,0x5b,0x5f,0x70,0x7f,0x7b};

Теперь, если нам нужно вывести какую-то цифру на дисплей, от нуля до девяти, что можно воспользоваться функцией:

void ht1621_send_digit(uint8_t pos,uint8_t digit) {
    ht1621_send_data((pos<<1),digits[digit]);
}

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

void ht1621_print(uint16_t num) {
    uint8_t first=1;
    PC_ODR &= ~(1<<CS);
    do {
        uint8_t rm=num % 10;

        if (first) {
          ht1621_send(0xa0, 3);
          ht1621_send(0, 6);
          ht1621_send(digits[rm], 8);
          first=0;
        } else {
          ht1621_send(digits[rm], 8);
        }
        num=num/10;
    } while (num>0);
    PC_ODR |=  (1<<CS);
}

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

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

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

#define LED (1<<5)
uint16_t count;
void tim4_irq(void) __interrupt(23) {
    if (count)
        --count;

    TIM4_SR=0;
}

void delay_ms(uint16_t ms)
{
    count = ms;
    TIM4_SR   = 0x0;                        // Clear Pending Bit
    TIM4_PSCR = TIM4_PRESCALER_128;         // =7, prescaler =128
    TIM4_ARR  = 15;                         // freq Timer IRQ ~1kHz
    TIM4_IER  = (uint8_t)TIM4_IT_UPDATE;    // =1, enable interrupt
    TIM4_CR1  = TIM4_CR1_CEN;               // =1, enable counter
    while(count) {
        wfi();                                  // goto sleep
    }
    TIM4_CR1  = 0x0;                        //  disable counter
}

int main() {
    CLK_CKDIVR=(uint8_t)0x18;               // HSI= 2MHz
    // Setup GPIO
    PB_DDR|=(LED);
    PB_CR1|=(LED);
    PC_DDR = ((1<<CS) | (1<<WR) | (1<<DATA)); // Push-Pull Mode
    PC_CR1 = ((1<<CS) | (1<<WR) | (1<<DATA)); //
    PC_CR2 = ((1<<CS) | (1<<WR) | (1<<DATA)); // Speed up 10 MHz
    // main loop
    ht1621_init();
    ht1621_clear();
    enableInterrupts();
    uint16_t tick=0;
    for(;;)
    {
        PB_ODR |= (LED);
        delay_ms(500);
        PB_ODR &= ~(LED);
        delay_ms(500);
        ht1621_print_num(tick++);
    }
}

Теперь, когда мы разобрались с дисплеем, давайте переходить к микроконтроллеру STM8L151C8.

3) Структура базового проекта с мигающим светодиодом

Для начала нам предстоит несколько рутинный процесс создания базового проекта в среде STVD с использованием компилятора Cosmic. Процесс пошагово описывался мною пару лет назад в главе: "Создание базового проекта в среде разработки STVD+COSMIC". Cosmic компилятор бесплатный, но с ограниченной лицензией сроком на один год и привязкой к определенному компьютеру.

Итак, создаем базовый проект мигалки под микроконтроллер STM8L151C8. Структура проекта на этом этапе будет следующей:

$ tree .
.
├── 01_stm8l151_blink.dep
├── 01_stm8l151_blink.stp
├── Debug
│   ├── 01_stm8l151_blink.lkf
│   └── 01_stm8l151_blink.sm8
├── inc
│   ├── stm8l_tim4.h
│   └── stm8l151c.h
├── main.c
├── Release
│   └── 01_stm8l151_blink.lkf
├── stm8_interrupt_vector.c
├── stm8l151c.s
└── utils.s

3 directories, 11 files

Здесь "main.c" - основной файл проекта на Си, "stm8l_tim4.h" - заголовочный файл на Си с константами из SPL, "stm8l151c.h" - заголовочный файл на Си с периферийными регистрами. В "stm8_interrupt_vector.c" находится таблица векторов прерывания, "utils.s" - это наш основной файл на Ассемблере STM8, "stm8l151c.s" - это ассемблерный файл с периферийными регистрами.

Файл "main.c" будет содержать инициализацию GPIO тестового светодиода и главный цикл, в котором будет производиться переключение данного светодиода:

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

#define LED 0     // PB0

extern void delay_ms(uint16_t value);

main()
{
    // ------- CLK Setup ---------------
    CLK_CKDIVR = 0;         // SYSCLK=16MHz
    // ------- GPIO setup --------------
    // LED
    PB_DDR |= (1<<LED);
    PB_CR1 |= (1<<LED);

    for(;;) {
        PB_ODR ^=(1<<LED);
        delay_ms(1000);
    }
}

Ассемблерный файл "utils.s" будет содержать реализацию функции задержки на 1 мс "delay_ms()":

    switch .text
    ; ------------------
    xref _delay_ms
    include "stm8l151c.s"

_delay_ms:
    pushw x
    pushw y
start_delay:
    ldw y, #3992      ; ->((500-1)=499*8) (16MHz)
loop:
    subw y,#1
    jrne loop
    decw x
    jrne start_delay
    popw y
    popw x
    ret

    end

Принципиальная схема на данный момент выглядит так:

Давайте посмотрим на энергопотребление микроконтроллера с такой программой. Со светодиодом на ножке PB0:

И без светодиода:

На светодиоде установлен токоограничивающий резистор в 2 кОм, поэтому его потребление практически не выделяется на фоне потребления самого микроконтроллера. 200мкА приходится на светодиод и 4583 мкА на сам микроконтроллер.

Первым методом снижения энергопотребления, который мы применим, будет "парковка" свободных GPIO. Из HiZ состояния переведем их в Push Pull режим, и подключим к "земле". Для этого, в начало функции main() добавим следующий код:

    // parking GPIO
    // to ground Port A
    PA_DDR = 0xFF;
    PA_CR1 = 0xFF;
    PA_ODR = 0;
    // to ground Port B
    PB_DDR = 0xFF;
    PB_CR1 = 0xFF;
    PB_ODR = 0;
    // to ground Port C
    PC_DDR = 0xFF;
    PC_CR1 = 0xFF;
    PC_ODR = 0;
    // to ground Port D
    PD_DDR = 0xFF;
    PD_CR1 = 0xFF;
    PD_ODR = 0;
    // to ground Port E
    PE_DDR = 0xFF;
    PE_CR1 = 0xFF;
    PE_ODR = 0;
    // to ground Port F
    PF_DDR = 0xFF;
    PF_CR1 = 0xFF;
    PF_ODR = 0;

Замеряем еще раз энергосбережения со светодиодом и без:

Как видно, с помощью этого нехитрого приёма, средний потребляемый ток уменьшился примерно на 600 мкА.

4) Использование Wait режима энергосбережения

Чтобы кардинально снизить энергопотребление, нужно использовать один из доступных режимов снижения энергопотребления. Начнем с Wait режима, как самого простого. Для того, чтобы войти в Wait режим, следует выполнить ассемблерную инструкцию "WFI" или "WFE". Выход из Wait режима после инструкции "WFI" происходит по наступлению прерывания. Выход из Wait режима после инструкции "WFE" происходит по наступлению события(Event'а). Мы пока будем использовать первый вариант - WFI.

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

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

    for(;;) {
        PB_ODR ^=(1<<BLUE_LED);
        wfi();
    }

В функцию "main()" добавляем код инициализации таймера TIM1:

    //====== TIM1 Setup ==========
    TIM1_SR1   = 0x0;                       // Clear Pending Bit
    TIM1_CR1   = 0x0;                       // Clear TIM1_CR1
    TIM1_CR2   = 0x0;                       // Clear TIM1_CR2
    TIM1_PSCRH = 0x0;  TIM1_PSCRL = 63;     // Prescaler = 64
    TIM1_ARRH  = 0x7a; TIM1_ARRL  = 0x12;   // (2*10^6)/prescaler(=64) =31250 -> 0x7A12 -> freq Timer IRQ =1Hz
    TIM1_IER   = 0x01;                      // set UIE flag, enable interrupt
    TIM1_CR1  |= 0x01;                      // set CEN flag, start timer

В данном случае. инициализация не сильно отличается от таковой для таймера TIM4. Разница лишь в том, что регистры TIM1_ARR и TIM1_PSCR 16-битные. Так же в качестве предделителя в TIM1_PSCR записывается не степень двойки, а выражение =(предделитель минус единица):

Из флагов таймера, нам потребовалось установить флаг UIE для разрешения прерывания:

В самом начале функции "main()" включаем тактирование таймера TIM1:

    CLK_PCKENR2 |=  0x02;                   // Enable TIM1

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

    switch .text
    ; ------------------
    xdef _tim1_handler
    include "stm8l151c.s"

    ; --- TIM4 HANDLER -----
_tim1_handler:
    bres TIM1_SR1,#0        ; clear pending flag (UIF)
    iret

    end

В обработчике прерывания нам потребуется очищать флаг UIF:

Обработчик прерывания TIM1 надо будет добавить в таблицу прерываний микроконтроллера:

/*  BASIC INTERRUPT VECTOR TABLE FOR STM8 devices
 *  Copyright (c) 2007 STMicroelectronics
 */

typedef void @far (*interrupt_handler_t)(void);

struct interrupt_vector {
    unsigned char interrupt_instruction;
    interrupt_handler_t interrupt_handler;
};

@far @interrupt void NonHandledInterrupt (void)
{
    /* in order to detect unexpected events during development, 
       it is recommended to set a breakpoint on the following instruction
    */
    while(1);
    return;
}

extern void _stext();     /* startup routine */
extern void tim1_handler();

struct interrupt_vector const _vectab[] = {
    {0x82, (interrupt_handler_t)_stext}, /* reset */
    {0x82, NonHandledInterrupt}, /* trap  */
    {0x82, NonHandledInterrupt}, /* irq0  TLI  */
    {0x82, NonHandledInterrupt}, /* irq1  FLASH */
    {0x82, NonHandledInterrupt}, /* irq2  DMA1 0/1 */
    {0x82, NonHandledInterrupt}, /* irq3  DMA1 2/3 */
    {0x82, NonHandledInterrupt}, /* irq4  RTC/LSE_CSS */
    {0x82, NonHandledInterrupt}, /* irq5  EXTI E/F/PVD */
    {0x82, NonHandledInterrupt}, /* irq6  EXTI B/G */
    {0x82, NonHandledInterrupt}, /* irq7  EXTI D/H */
    {0x82, NonHandledInterrupt}, /* irq8  EXTI 0 */
    {0x82, NonHandledInterrupt}, /* irq9  EXTI 1 */
    {0x82, NonHandledInterrupt}, /* irq10 EXTI 2 */
    {0x82, NonHandledInterrupt}, /* irq11 EXTI 3*/
    {0x82, NonHandledInterrupt}, /* irq12 EXTI 4*/
    {0x82, NonHandledInterrupt}, /* irq13 EXTI 5 */
    {0x82, NonHandledInterrupt}, /* irq14 EXTI 6*/
    {0x82, NonHandledInterrupt}, /* irq15 EXTI 7*/
    {0x82, NonHandledInterrupt}, /* irq16 LCD */
    {0x82, NonHandledInterrupt}, /* irq17 CLK/TIM1/DAC */
    {0x82, NonHandledInterrupt}, /* irq18 COMP1/COMP2/ADC */
    {0x82, NonHandledInterrupt}, /* irq19 TIM2/USART2 */
    {0x82, NonHandledInterrupt}, /* irq20 TIM2/USART2 */
    {0x82, NonHandledInterrupt}, /* irq21 TIM3/USART3 */
    {0x82, NonHandledInterrupt}, /* irq22 TIM3/USART3 */
    {0x82, (interrupt_handler_t)tim1_handler}, /* irq23 TIM1 */
    {0x82, NonHandledInterrupt}, /* irq24 TIM1 */
    {0x82, NonHandledInterrupt}, /* irq25 TIM4 */
    {0x82, NonHandledInterrupt}, /* irq26 SPI1 */
    {0x82, NonHandledInterrupt}, /* irq27 USART1/TIM5 */
    {0x82, NonHandledInterrupt}, /* irq28 USART1/TIM5 */
    {0x82, NonHandledInterrupt}, /* irq29 I2C1/SPI2 */
};

Кроме всего прочего, на данном этапе в проект добавляется заголовочный файл "main.h":

#define BLUE_LED -1         // PB0
#define DS  1       // PB1

#define EnableInterrupt()   _asm("rim\n");
#define DisableInterrupt()  _asm("sim\n");
#define wfi()               _asm("wfi\n");

Компилируем проект, загружаем прошивку в микроконтроллер и смотрим на статистику энергопотребления без подключенного светодиода:

Как видим, энергопотребление упало в пять раз 750 мкА. Это уже что-то.

Мы можем снизить энергопотребление микроконтроллера еще больше, если снизим тактовую частоту микроконтроллера, скажем, до 1 MHz.

Для этого, в модуле "main.c" строку:

CLK_CKDIVR = 0;         // SYSCLK=16MHz

заменим на:

CLK_CKDIVR = 4;         // SYSCLK=1MHz

Смотрим энергопотребление:

И видим, что энергопотребление снизилось еще в два раза, до 329 мкА.

5) Подключение дисплея HT1621 к микроконтроллеру STM8L151C8 и оценка энергопотребления

Теперь давайте подключим дисплей к микроконтроллеру. Схема подключения пусть будет следующей:

Далее переносим ранее написанный драйвер дисплея в наш проект. Модуль "ht1621.c" будет выглядеть так:

#include <stdint.h>
#include "iostm8l152x.h"
#include "ht1621.h"

#define num_len 10

const uint8_t digits[10] = {0x7d,0x60,0x3e,0x7a,0x63,0x5b,0x5f,0x70,0x7f,0x7b};

void ht1621_send_cmd(uint8_t cmd);
void ht1621_send(uint8_t data, uint8_t len);
void ht1621_send_data(uint8_t adr, uint8_t value);
uint8_t ht1621_char_to_seg_bits(uint8_t ch);

void ht1621_init(void) {
    ht1621_send_cmd(BIAS);
    ht1621_send_cmd(RC256);
    ht1621_send_cmd(SYSDIS);
    ht1621_send_cmd(WDTDIS1);
    ht1621_send_cmd(SYSEN);
    ht1621_send_cmd(LCDON);
}

void ht1621_clear(void) {
    char i;
    for(i=0;i<16;i++) {
        ht1621_send_data((i<<1),0x0);
    }
}

void ht1621_print(uint16_t num) {
    uint8_t first=1;
    PC_ODR &= ~(1<<CS);
    do {
        uint8_t rm=num % 10;

        if (first) {
          ht1621_send(0xa0, 3);
          ht1621_send(0, 6);
          ht1621_send(digits[rm], 8);
          first=0;
        } else {
          ht1621_send(digits[rm], 8);
        }
        num=num/10;
    } while (num>0);
    PC_ODR |=  (1<<CS);
}

void ht1621_send_data(uint8_t adr, uint8_t value) {
    adr <<=2;
    PB_ODR &= ~(1<<CS);
    ht1621_send(0xa0, 3);
    ht1621_send(adr, 6);
    ht1621_send(value, 8);
    PB_ODR |=  (1<<CS);
}

void ht1621_send_cmd(uint8_t cmd) {
    PB_ODR &= ~(1<<CS);
    ht1621_send(0x80,4);
    ht1621_send(cmd,8);
    PB_ODR |=  (1<<CS);
}

void ht1621_send(uint8_t data, uint8_t len) {
    char i;
    for(i=0;i<len;i++) {
        PB_ODR=(data & 0x80) ? PB_ODR | (1<<DATA) : PB_ODR & ~(1<<DATA);
        PB_ODR &= ~(1<<WR);
        PB_ODR |= (1<<WR);
        data=(data<<1);
    }
}

а модуль "main.c" так:

#include <stdint.h>
#include "iostm8l152x.h"
#include "ht1621.h"
#include "main.h"

main()
{
    uint16_t tick=0;
    // ------- CLK Setup ---------------
    CLK_DIVR = 4;         // SYSCLK=1MHz
    CLK_PCKENR2 |=  0x02;                   // Enable TIM1
    // parking GPIO
    // to ground Port A
    PA_DDR = 0xFF; PA_CR1 = 0xFF; PA_ODR = 0;
    // to ground Port B
    PB_DDR = 0xFF; PB_CR1 = 0xFF; PB_ODR = 0;
    // to ground Port C
    PC_DDR = 0xFF; PC_CR1 = 0xFF; PC_ODR = 0;
    // to ground Port D
    PD_DDR = 0xFF; PD_CR1 = 0xFF; PD_ODR = 0;
    // to ground Port E
    PE_DDR = 0xFF; PE_CR1 = 0xFF; PE_ODR = 0;
    // to ground Port F
    PF_DDR = 0xFF; PF_CR1 = 0xFF; PF_ODR = 0;
    // ------- GPIO setup --------------
    // Soft SPI GPIO Setup
    PB_DDR = ((1<<CS) | (1<<WR) | (1<<DATA)); // Push-Pull Mode
    PB_CR1 = ((1<<CS) | (1<<WR) | (1<<DATA)); //
    PB_CR2 = ((1<<CS) | (1<<WR) | (1<<DATA)); // Speed up 10 MHz
    // LED
    PB_DDR |= (1<<LED);
    PB_CR1 |= (1<<LED);
    //====== TIM1 Setup ==========
    TIM1_SR1   = 0x0;                       // Clear Pending Bit
    TIM1_CR1   = 0x0;                       // Clear TIM1_CR1
    TIM1_CR2   = 0x0;                       // Clear TIM1_CR2
    TIM1_PSCRH = 0x0;  TIM1_PSCRL = 63;     // Prescaler = 64
    TIM1_ARRH  = 0x7a; TIM1_ARRL  = 0x12;   // (2*10^6)/prescaler(=64) =31250 -> 0x7A12 -> freq Timer IRQ =1Hz
    TIM1_IER   = 0x01;                      // set UIE flag, enable interrupt
    TIM1_CR1  |= 0x01;                      // set CEN flag, start timer
    // main loop
    ht1621_init();
    ht1621_clear();

    for(;;) {
        PB_ODR ^=(1<<LED);
        ht1621_print(tick++);
        _asm("wfi");
    }
}

Теперь давайте оценим энергопотребление с подключенным дисплеем(слева) и без него(справа):

Светодиод был отключен в обоих вариантах. И разница в потреблении составляет 222 мкА. Это цифра - потребление дисплея, и она вдвое меньше заявленной изначально 0.4мА, на что я собственно и рассчитывал.

Полный код примера можно посмотреть здесь: https://gitlab.com/flank1er/stm8l_ht1621_clock/-/tree/main/01_blink

6) Добавляем энкодер на таймере TIM2

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

Энкодер настраивается и работает абсолютно также как и в STM32 "STM32F103C8 без HAL и SPL: Работа с SPI дисплеями. Добавляем энкодер на таймере TIM2", но здесь есть некоторая загвоздка. Согласно Reference Manual RM0031, работать в режиме энкодера в STM8L может только таймер TIM1, которого в STM8L051f3 нет. К счастью, на easyelectronics.ru нашлась информация, что в режиме энкодера может работать таймер TIM2: "STM8L encoder mode". Я проверял на STM8L151C8 и на STM8L051F3 и информация подтвердилась. Для нас это все значительно упрощает.

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

Схема подключения будет следующей:

Каналы Ch1 и Ch2 таймера TIM2 выведены на пины PB0 и PB2. К ним подключаем энкодер. Светодиод, который был на PB0 убираем, нам он больше не нужен. Кнопку энкодера мы пока использовать не будем, но можно заодно подключить и ее на PB1.

Нам потребуется модифицировать функцию "ht1621_print()", чтобы она могла выводить отрицательные значения:

void ht1621_print(int num) {
    uint16_t value=get_abs(num);
    uint8_t first=1;

    if (num <= 0)
      ht1621_clear();

    PB_ODR &= ~(1<<CS);
    do {
        uint8_t rm=value % 10;

        if (first) {
          ht1621_send(0xa0, 3);
          ht1621_send(0, 6);
          ht1621_send(digits[rm], 8);
          first=0;
        } else {
          ht1621_send(digits[rm], 8);
        }
        value=value/10;
    } while (value>0);

    if (num < 0)
      ht1621_send(0x2,8);           // print "minus"


    PB_ODR |=  (1<<CS);
}

Модуль "main.c" примет следующий вид:

#include <stdint.h>
#include "iostm8l152x.h"
#include "ht1621.h"
#include "main.h"

//extern void delay_ms(uint16_t value);

main()
{
    int count=0;
    uint8_t tick=0;
    // ------- CLK Setup ---------------
    CLK_DIVR = 4;           // SYSCLK=1MHz
    CLK_PCKENR2 |= 0x02;    // Enable TIM1
    CLK_PCKENR1 |= 0x01;    // Enable TIM2      

    // parking GPIO
    // to ground Port A
    PA_DDR = 0xFF; PA_CR1 = 0xFF; PA_ODR = 0;
    // to ground Port B
    PB_DDR = 0xFF; PB_CR1 = 0xFF; PB_ODR = 0;
    // to ground Port C
    PC_DDR = 0xFF; PC_CR1 = 0xFF; PC_ODR = 0;
    // to ground Port D
    //PD_DDR = 0xFF; PD_CR1 = 0xFF; PD_ODR = 0;
    // to ground Port E
    PE_DDR = 0xFF; PE_CR1 = 0xFF; PE_ODR = 0;
    // to ground Port F
    PF_DDR = 0xFF; PF_CR1 = 0xFF; PF_ODR = 0;

    // ------- GPIO setup --------------
    // Soft SPI GPIO Setup
    PB_DDR = ((1<<CS) | (1<<WR) | (1<<DATA)); // Push-Pull Mode
    PB_CR1 = ((1<<CS) | (1<<WR) | (1<<DATA)); //
    PB_CR2 = ((1<<CS) | (1<<WR) | (1<<DATA)); // Speed up 10 MHz
    // Encoder
    PB_DDR &= ~((1<<ENC_L) | (1<<ENC_R));
    PB_CR1 |=  ((1<<ENC_L) | (1<<ENC_R));

    // ====== TIM1 Setup =========
    TIM1_SR1   = 0x0;                       // Clear Pending Bit
    TIM1_CR1   = 0x0;                       // Clear TIM1_CR1
    TIM1_CR2   = 0x0;                       // Clear TIM1_CR2
    TIM1_PSCRH = 0x0;  TIM1_PSCRL = 3;     // Prescaler = 64
    TIM1_ARRH  = 0x7a; TIM1_ARRL  = 0x12;   // (10^6)/prescaler(=64) =31250 -> 0x7A12 -> freq Timer IRQ =1Hz
    TIM1_IER   = 0x01;                      // set UIE flag, enable interrupt
    TIM1_CR1  |= 0x01;                      // set CEN flag, start timer
    // ======= TIM2 Setup =========
    TIM2_CCER1|= 0x22;  // CC2P,CC1P: Capture/compare 2 output polarity
    TIM2_CCMR1|= 0x01;  // CC1 channel is configured as input, IC1 is mapped on TI1FP1
    TIM2_CCMR2|= 0x01;  // CC2 channel is configured as input, IC2 is mapped on TI2FP2
    TIM2_SMCR |= 0x01;  // encoder mode 1
    TIM2_ARRH=1;        // max value counter =500
    TIM2_ARRL=0xf4;     // max value counter =500
    TIM2_CNTRH=0;       // clear counter
    TIM2_CNTRL=0;       // clear counter
    TIM2_CR1|=0x1;      // enable counter
    // main loop
    ht1621_init();
    ht1621_clear();
    ht1621_print(0);
    for(;;) {
        if (tick == 20 ) {
          //PB_ODR ^=(1<<LED);
          tick=0;
        } else {
          tick++;
        }

        count=(uint16_t)TIM2_CNTRH;
        count = count <<8;
        count |=(uint16_t)TIM2_CNTRL;
        count = count >>1;
        if (count >= 125) {
          count = (count - 250);
        }

        ht1621_print(count);

        //delay_ms(50);
        _asm("wfi");
    }
}

Где ENC_L и ENC_R это 0 и 2.

Полный код можно посмотреть здесь: https://gitlab.com/flank1er/stm8l_ht1621_clock/-/tree/main/02_encoder

7) Программа часов

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

Мне хотелось написать вариант программы максимально приближенный для работы на STM8L051F3, по этой причине вместо таймера TIM1, которого нет в STM8L051f3, используется TIM3. Также я полностью отказался от прерываний, вместо них используется механизм Event'ов. О них я буду рассказывать в следующей главе, а сейчас я хотел бы сконцентрироваться на алгоритме работе часов.

Полный код примера можно посмотреть здесь: https://gitlab.com/flank1er/stm8l_ht1621_clock/-/tree/main/03_clock

Итак, время выводится на дисплей в формате "ЧЧ-MM", где знак минус выполняющий роль разделителя мигает с интервалом в одну секунду. Секунды на дисплей не выводятся. Время считается в минутах, и по достижении 1440 минут (одни сутки), счетчик обнуляется. Соответственно, счет минут ведет одна переменная типа uint16_t - "time", а счет долей секунд ведет другая переменная - "tick".

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

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

Главный цикл работает с частой 5 - 10 раз в секунду, т.к. он постоянно находится в ожидании нажатия кнопки энкодера, которая переключает режимы работы часов. Т.к. нам нельзя пользоваться прерываниями, кнопка считывается методом опроса порта.

 76     for(;;) {
 77       // if press button        
 78       btn=PB_IDR;
 79       btn &= 0x2;

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

Таким образом, если кнопка нажата, переключается режим работы (переменная "menu") и обнуляется счетчик таймера энкодера. После этого программа ждет в цикле когда копка будет отпущена:

 80       if (btn == 0) {
 81         menu = (menu<2) ? menu +1 : 0;
 82         TIM2_CNTRL=0;
 83         TIM2_CNTRH=0;
 84         //ht1621_print((int)menu);
 85 
 86         do {
 87           btn=PB_IDR;
 88           btn &= 0x2;
 89           delay_ms(300);
 90         } while (btn == 0);
 91       }

Если производится установка времени, т.е. (menu != 0), то считывается значение энкодера:

 93       // get encoder data
 94       if (menu) {
 95         count=(uint16_t)TIM2_CNTRH;
 96         count = count <<8;
 97         count |=(uint16_t)TIM2_CNTRL;
 98         count = count >>1;
 99 
100         if (count >= 125) {
101           count = (count - 250);
102         }
103       }

Здесь "count" - значение энкодера, это переменная со знаком.

Частота работы главного цикла задается таймером TIM3, т.к. таймера TIM1 в STM8L051F3 нет.

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

Частоту главного цикла меняет следующий блок:

105       if (menu==2 && prev == 1) {
106         time =offset;
107       } else if (menu == 0 && prev == 2){
108         TIM3_CR1&=~(0x01);  // disable timer
109         TIM3_ARRH  = 0x0c;
110         TIM3_ARRL  = 0x35;
111         TIM3_CNTRH=0;       // clear counter
112         TIM3_CNTRL=0;       // clear counter
113         TIM3_CR1 |= (0x01); // enable timer
114         time =offset;
115         tick=0;
116       } else if (menu == 1 && prev == 0) {
117         TIM3_CR1&=~(0x01);  // disable timer
118         TIM3_ARRH  = 0x03;
119         TIM3_ARRL  = 0xe8;
120         TIM3_CNTRH=0;       // clear counter
121         TIM3_CNTRL=0;       // clear counter
122         TIM3_CR1 |= (0x01); // enable timer
123         tick=0;
124       }

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

127       if (!(++tick%5)) {        // last 1 sec
128         blink=!blink;
129         switch (menu) {
130           case 1:
131             offset = count * 60;
132             offset += (time/60)*60;
133             offset = get_abs(offset);
134             offset += time%60;
135             offset %= 1440;
136             ht1621_hour(offset,blink);
137             tick=0;
138             break;
139           case 2:
140             offset = time+count;
141             ht1621_minute(offset,blink);
142             tick=0;
143             break;
144           default:
145             ht1621_time(time,blink);
146         }
147       }

Здесь в нормальном режиме одна секунда равна пяти проходам главного цикла. Переменная blink отвечает за мигание. В данном случае, для каждого из трех режимов есть своя функция вывода на дисплей. При переносе кода на STM8L051F3 я объединил их в одну. Еще здесь есть один подводный камень связанный с умножением. Умножение знакового числа Cosmic производит с помощью библиотечной функции. И это означает, что нам придется как-то хитрить, когда будем переносить код на STM8L051F3.

Эти четыре последовательных блока: 1) обработка нажатия кнопки; 2) получение данных с энкодера; 3) управление частотой главного цикла; 4) вывод информации на дисплей, являются основой алгоритма часов. Оставшийся кусок кода главного цикла тривиален:

149       // time count   
150       if (!menu) {
151         if (tick == 300) {      // if 1 minute
152           ++time;
153           tick=0;
154         }
155         if (time == 1440)       // if 24 hour
156           time=0;
157       }
158 
159 
160       prev=menu;
161       _asm("wfe");
162       TIM3_SR1 =0;
172     }

Вот последние две строчки, что выделены красным, не так тривиальны, как может показаться на первый взгляд. О них придется поговорить по-подробнее.

8) Механизм Event'ов и режим энергосбережения флеш-памяти IDDQ

Режимы энергосбережения STM8L можно разделить на три группы: Wait, Low Power и Halt. Режим Wait мы уже использовали. Режим Halt тоже должен быть всем понятен. В этом режиме выключается все или все за исключением RTC. Наибольший интерес на мой взгляд представляет режим Low Power (выделенно зеленым):

В режиме Low Power мы имеем рабочий CPU, активную периферию (за исключением АЦП, он должен быть отключен), мы можем пользоваться Event'ами и при этом энергопотребление у нас будет на уровне Halt режима. В документации указан следующий порядок перехода в режим энергосбережения "Low Power Run Mode":

Т.е. сначала потребуется перенести исполняемый код в оперативную память, затем переключить тактирование на LSI или LSE. Отключить все высокочастотные генераторы, АЦП и всю неиспользуемую периферию. Отключить флеш-память установкой флага EEPM в регистре FLASH_CR1. Подождать какое-то время, чтобы гарантировать отключение флеш-памяти, и сконфигурировать регулятор напряжения в "Ultra Low Power" режим установкой флага REGOFF в регистре CLK_REGCGR. Для перехода в "Low Power Wait Mode" потребуется еще выполнить инструкцию "WFE".

В данном случае нам потребуется главный цикл оформить в виде функции и уже от туда осуществлять переход в Low Power режим энергосбережения. Как функция переноситься в ОЗУ средствами компилятора Cosmic я рассказывал в статье - "Копирование кода в ОЗУ и выполнение его оттуда средствами COSMIC". Потребуется добавить в проект ассемблерный файл "fctcpy.s", добавить новую секцию линковщику в свойствах проекта, а саму функцию поместить в новую секцию с помощью директив "#pragma section()".

Однако, тут встает вопрос о том, как быть с прерываниями. Если в STM32 таблицу прерываний можно перенести в ОЗУ, то в STM8 это технически невозможно. И тут на замену прерываниям приходят Event'ы (события). Event'ы работают почти также как прерывания, но обработчик прерывания в этом случае не вызывается. В случае с Event'ами вместо инструкции "WFI" применяется инструкция "WFE". И после инструкции "WFE" мы должны выяснить какой Event вызвал пробуждение из спящего режима, после чего переходить на код обработчика прерывания. Т.е. тот код, который должен выполняться при наступлении прерывания.

Для того, чтобы включить Event для таймера TIM3 по обновлению счетчика, следует выставить первый бит в регистре WFE_CR3:

Настройка таймера производится почти также как и при работе от прерывания, добавляется только включение Event'a:

 50     // ====== TIM3 Setup =========
 51     TIM3_SR1   = 0x0;                       // Clear Pending Bit
 52     TIM3_CR1   = 0x0;                       // Clear TIM1_CR1
 53     TIM3_CR2   = 0x0;                       // Clear TIM1_CR2
 54     TIM3_PSCR  = 6;                         // Prescaler 2^6= 64
 55     TIM3_ARRH  = 0x0c; TIM3_ARRL  = 0x35;   // (10^6)/64(prescaler) =15625 -> 0x7A12 -> freq Timer IRQ =1Hz
 56     TIM3_IER   = 0x01;                      // set UIE flag, enable interrupt
 57     TIM3_CNTRH=0;                           // clear counter
 58     TIM3_CNTRL=0;                           // clear counter
 59     WFE_CR3   |= 0x1;                       // enable event
 60     TIM3_CR1  |= 0x01;                      // set CEN flag, start timer    

И при переходе в спящий режим вызывается инструкция WFE вместо WFI:

161       _asm("wfe");
162       TIM3_SR1 =0;

По выходу из спящего режима, выполняется код обработчика прерывания, в данном случае сбрасывается флаг UIF.

С флеш-памятью дело обстоит так. В главе 3.7 руководства "STM8L Reference Manual (RM0031)" описан режим энергосбережения флеш-памяти IDDQ, при котором флеш-память отключается, а выполнение программного кода осуществляется из ОЗУ. Если мы посмотри на флаги регистра FLASH_CR1:

То здесь увидим флаг EEPM который следует установить в процессе перехода в режимы энергосбережения: "Low Power Run Mode" и "Low Power Wait Mode".

Режим IDDQ флеш-памяти можно использовать и при использовании обычного Wait режима энергосбережения. Для этого следует выставить флаг WAITM. Тогда во время спящего режима флеш-память будет переходить в IDDQ режим энергосбережения, но фактически это мало что даст, где-то 50-80 мкА снижения энергопотребления. Чтобы кардинально снизить энергопотребление, нужно использовать режимы энергосбережения Low Power Run/Wait Mode или (Active) Halt.

9) Переходим в режим Low Power Wait Mode с тактированием от LSI

Теперь, когда мы наконец-то разобрались с теорией и дисплеем, можно уже написать программу, которая будет работать в режиме энергосбережения "Low Power Wait Mode". Для начала будем использовать тактирование от встроенного LSI, и в качестве примера работы дисплея возьмем опять простой счетчик.

Полный код примера можно посмотреть здесь: https://gitlab.com/flank1er/stm8l_ht1621_clock/-/tree/main/04_low_power

Секция "pragma" которая загружается в оперативную память выглядит довольно скромно по объему. Она содержит как функции работы с дисплеем, так и главный цикл:

#pragma section(FLASH_CODE)
void ht1621_print(int num) {

    uint16_t value=get_abs(num);
    uint8_t first=1;

    if (num <= 0)
      ht1621_clear();

    PB_ODR &= ~(1<<CS);
    do {
        uint8_t rm=value % 10;

        if (first) {
          ht1621_send(0xa0, 3);
          ht1621_send(0, 6);
          ht1621_send(digits[rm], 8);
          first=0;
        } else {
          ht1621_send(digits[rm], 8);
        }
        value=value/10;
    } while (value>0);

    if (num < 0)
      ht1621_send(MINUS,8);


    PB_ODR |=  (1<<CS);
}

void ht1621_send_data(uint8_t adr, uint8_t value) {
    adr <<=2;
    PB_ODR &= ~(1<<CS);
    ht1621_send(0xa0, 3);
    ht1621_send(adr, 6);
    ht1621_send(value, 8);
    PB_ODR |=  (1<<CS);
}

void ht1621_send_cmd(uint8_t cmd) {
    PB_ODR &= ~(1<<CS);
    ht1621_send(0x80,4);
    ht1621_send(cmd,8);
    PB_ODR |=  (1<<CS);
}

void ht1621_send(uint8_t data, uint8_t len) {
    char i;
    for(i=0;i<len;i++) {
        PB_ODR=(data & 0x80) ? PB_ODR | (1<<DATA) : PB_ODR & ~(1<<DATA);
        PB_ODR &= ~(1<<WR);
        PB_ODR |= (1<<WR);
        data=(data<<1);
    }
}


void main_loop(void) {
    uint16_t tick=0;
    uint8_t i=30;
    ht1621_init();
    ht1621_clear();
    ht1621_print(0);
    // Switch to LSI (38kHz)
    CLK_SWCR |= (1<<1);                     // set SWEN flag
    CLK_ICKCR |= (1<<2);                    // set LSION flag
    while (!(CLK_ICKCR & 0xf7));            // wait LSIRDY flag
    CLK_SWR= 0x02;                          // set LSI
    CLK_ICKCR &= ~(1<<0);                   // reset HSION flag
    //==== FLASH Switch Off =======
    FLASH_CR1 |= (1<<3);                    // set EEPM flag
    while (--i > 0);                        // wait after switch off flash memory
    // === MVR Swtch Off ==========
    CLK_REGCSR |= (1<<1);                   // set REGOFF flag
    // === main loop =========
    //FLASH_CR1 |= (1<<2); // set WAITM flag
    TIM4_CR1  |= 0x01;
    for(;;) {
      _asm("wfe");
      TIM4_SR1 =0;

      tick++;
      ht1621_print(tick);
    }
}
#pragma section()

Участок кода выделенный красным осуществляет переход в режим энергосбережения "Low Power Wait Mode". Т.е. реализует тот порядок действий, который был описан в Reference Manual:

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

Значения переменной "uint8_t i=30", которая устанавливает время задержки после переключения флеш-памяти в режим IDDQ, было подобрано экспериментальным способом и может варьироваться.

Код содержит ошибку в строку "while (!(CLK_ICKCR & 0xf7));", ее мы исправим когда будем использовать LSE. В принципе код работает. Еще я забыл обнулить регистр "CLK_DIVR" который содержит четвертку и делит частоту на 16. т.е. вместо частоты 38кГц мы получаем 38/16 = 2,375 кГц. Как мы уже выяснили, каждая цифра на дисплее занимает две ячейки памяти в контроллере ht1621, и на такой частоте можно глазом уловить как меняется сначала одна часть цифры, а затем другая.

Для нас сейчас это все не важно, работает и ладно. Еще хочу обратить внимание, что для формирования задержек теперь используется таймер TIM4.

10) Переключение на тактирование от LSE

Для использования LSE нам следует подключить часовой кварц к микроконтроллеру. Он подключается на пины 44 и 45 чипа STM8L151C8, это ножки PC5 и PC6. Схема будет выглядеть так:

Полный код примера можно посмотреть здесь: https://gitlab.com/flank1er/stm8l_ht1621_clock/-/tree/main/05_use_lse

От предыдущего примера программа отличается лишь блоком переключения на LSE вместо LSI:

    // Switch to LSE (32,768 kHz)
    CLK_SWCR |= (1<<1);                     // set SWEN flag
    CLK_ECKCR |= (1<<2);                    // set LSEON flag
    while (!(CLK_ECKCR & 0x08));            // wait LSERDY flag
    CLK_SWR= 0x08;                          // set LSE
    while (CLK_SWCR & 0x01);                // wait to reset SWBSY flag
    CLK_ICKCR &= ~(1<<0);                   // reset HSION flag
    CLK_DIVR = 0;                           // SYSCLK=LSE
    //==== FLASH Switch Off =======
    FLASH_CR1 |= (1<<3);                    // set EEPM flag
    while (--i > 0);                        // wait after switch off flash memory
    // === MVR Swtch Off ==========
    CLK_REGCSR |= (1<<1);                   // set REGOFF flag

Если замерить энергопотребление, то получится такая картинка:

Двух пальчиковых батареек должно хватить на что-то около года службы. Более интересным мне показался график потребления:

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

11) Часы на микроконтроллере STM8L151C8

Микроконтроллер STM8L151C8 имеет 4 кБайт оперативной памяти, поэтому здесь будет не сложно заставить работать даже самый корявый алгоритм))

Полный код примера можно посмотреть здесь: https://gitlab.com/flank1er/stm8l_ht1621_clock/-/tree/main/06_lse_clock

Блок "pragma", размещаемый в оперативной памяти, и реализующий программу часов у меня получился таким:

#pragma section(FLASH_CODE)
static void ht1621_time(uint16_t t, uint8_t flag) {
  uint8_t d,z;
  d=(uint8_t)(t%60);
  z=(uint8_t)(d%10);
  ////////////////////
  PB_ODR &= ~(1<<CS);
  ht1621_send(0xa0, 3);
  ht1621_send(0, 6);
  t=t/60;
  if (menu == (uint8_t)0x1 && !flag) {
    ht1621_send(0, 8);
    ht1621_send(0, 8);
  }else {
    ht1621_send(digits[z], 8);
    z=(uint8_t)(d/10);
    ht1621_send(digits[z], 8);
  }
  if (flag || menu)
    ht1621_send(MINUS, 8);
  else
    ht1621_send(0,8);

  if (menu == (uint8_t)0x2 && !flag) {
    ht1621_send(0, 8);
    ht1621_send(0, 8);
  }else {
    z=(uint8_t)(t%10);
    ht1621_send(digits[z], 8);
    z=(uint8_t)(t/10);
    if (t>0)
      ht1621_send(digits[z], 8);
    else
      ht1621_send(0,8);
  }
  PB_ODR |= (1<<CS);
  //////////////////////
}

void ht1621_print(int num) {

    uint16_t value=get_abs(num);
    uint8_t first=1;

    if (num <= 0)
      ht1621_clear();

    PB_ODR &= ~(1<<CS);
    do {
        uint8_t rm=value % 10;

        if (first) {
          ht1621_send(0xa0, 3);
          ht1621_send(0, 6);
          ht1621_send(digits[rm], 8);
          first=0;
        } else {
          ht1621_send(digits[rm], 8);
        }
        value=value/10;
    } while (value>0);

    if (num < 0)
      ht1621_send(MINUS,8);


    PB_ODR |=  (1<<CS);
}

void ht1621_send_data(uint8_t adr, uint8_t value) {
    adr <<=2;
    PB_ODR &= ~(1<<CS);
    ht1621_send(0xa0, 3);
    ht1621_send(adr, 6);
    ht1621_send(value, 8);
    PB_ODR |=  (1<<CS);
}

void ht1621_send_cmd(uint8_t cmd) {
    PB_ODR &= ~(1<<CS);
    ht1621_send(0x80,4);
    ht1621_send(cmd,8);
    PB_ODR |=  (1<<CS);
}

void ht1621_send(uint8_t data, uint8_t len) {
    char i;
    for(i=0;i<len;i++) {
        PB_ODR=(data & 0x80) ? PB_ODR | (1<<DATA) : PB_ODR & ~(1<<DATA);
        PB_ODR &= ~(1<<WR);
        PB_ODR |= (1<<WR);
        data=(data<<1);
    }
}


void main_loop(void) {
    uint8_t prev=0;
    uint8_t btn_press=0;
    uint16_t i=40;
    uint8_t btn=0;
    uint16_t tick=0;
    uint16_t time=1000;
    uint8_t blink=0;
    ht1621_init();
    ht1621_clear();
    //ht1621_time(time,0);
    // Switch to LSE (32,768 kHz)
    CLK_SWCR |= (1<<1);                     // set SWEN flag
    CLK_ECKCR |= (1<<2);                    // set LSEON flag
    while (!(CLK_ECKCR & 0x08));            // wait LSERDY flag
    CLK_SWR= 0x08;                          // set LSE
    while (CLK_SWCR & 0x01);                // wait to reset SWBSY flag
    CLK_ICKCR &= ~(1<<0);                   // reset HSION flag
    CLK_DIVR = 0;                           // SYSCLK=LSE
    //==== FLASH Switch Off =======
    FLASH_CR1 |= (1<<3);                    // set EEPM flag
    while (--i > 0);                        // wait after switch off flash memory
    // === MVR Swtch Off ==========
    CLK_REGCSR |= (1<<1);                   // set REGOFF flag
    // === main loop =========
    //FLASH_CR1 |= (1<<2); // set WAITM flag
    TIM4_CR1  |= 0x01;
    ht1621_time(time,0);
    for(;;) {
      // if press button        
      btn=PB_IDR;
      btn &= 0x2;
      if (btn == 0) {
        menu = (menu<2) ? menu +1 : 0;
        TIM2_CNTRL=0;
        TIM2_CNTRH=0;

        do {
          btn=PB_IDR;
          btn &= 0x2;
          for(i=0; i<300;i++) {
            _asm("nop");
          }
        } while (btn == 0);
      }

      // get encoder data
      if (menu) {
        count=(uint16_t)TIM2_CNTRH;
        count = count <<8;
        count |=(uint16_t)TIM2_CNTRL;
        count = count >>1;

        if (count >= 125) {
          count = (count - 250);
        }
      }

      if (menu==2 && prev == 1) {
        time =offset;
        time=time;
      } else if (menu == 0 && prev == 2){
        TIM4_CR1&=~(0x01);  // disable timer
        TIM4_PSCR  = 0x0c;
        TIM4_CNTR=0;
        TIM4_CR1 |= (0x01); // enable timer
        time =offset;
        tick=0;
      } else if (menu == 1 && prev == 0) {
        TIM4_CR1&=~(0x01);  // disable timer
        TIM4_PSCR  = 0x0b;
        TIM4_CR1 |= (0x01); // enable timer
        tick=0;
      }


      if (!(++tick%4)) {        // last 1 sec
        blink=!blink;
        switch (menu) {
          case 2:
            offset = time/60;
            offset += count;
            offset = get_abs(offset);
            offset = (offset<<4)-offset;
            offset <<=2;
            offset += (time%60);
            ht1621_time(offset,blink);
            tick=0;
            break;
          case 1:
            offset = time+count;
            ht1621_time(offset,blink);
            tick=0;
            break;
          default:
            ht1621_time(time,blink);
        }
      }



      // time count   
      if (!menu) {
        if (tick == 240) {      // if 1 minute

          ++time;
          tick=0;
        }
        if (time == 1440)       // if 24 hour
          time=0;
      }//    else 
          //ht1621_time(time,blink);                  

      _asm("wfe");
      TIM4_SR1 =0;

      prev=menu;
    }
}
#pragma section()

Здесь хочется пояснить. 1) В функции "ht1621_time(uint16_t t, uint8_t flag)" я объединил предыдущие три функции вывода данных на дисплей. 2) Функция "void ht1621_print(int num)" здесь лишняя, она не используется.

Далее. Вот этот блок пришлось переработать:

130           case 1:
131             offset = count * 60;
132             offset += (time/60)*60;
133             offset = get_abs(offset);
134             offset += time%60;
135             offset %= 1440;
136             ht1621_hour(offset,blink);
137             tick=0;
138             break;

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

  x * 60 =  x * 15 * 4 = ((x *16)-x) * 4)

Ну и далее, умножение заменяем на сдвиг влево. В итоге получилось следующее:

            offset = time/60;
            offset += count;
            offset = get_abs(offset);
            offset = (offset<<4)-offset;
            offset <<=2;
            offset += (time%60);
            ht1621_time(offset,blink);
            tick=0;
            break;

12) Адаптация кода на микроконтроллер STM8L051F3

Теперь, кода этап разработки практически окончен, можно перенести прошивку на целевой 20-пиновый микроконтроллер STM8L051F3 c одним килобайтам оперативной памяти.

Принципиальная схема с микроконтроллером STM8L051F3 будет выглядеть как-то так:

Микроконтроллер STM8L051F3 можно прошить только 100 раз, особо не разбежишься.

Сначала я проверил проект часов с режимом энергосбережения Wait: "https://gitlab.com/flank1er/stm8l_ht1621_clock/-/tree/main/07_051_clock". Он заработал без каких-то плясок с бубном.

Следующие два проекта:
https://gitlab.com/flank1er/stm8l_ht1621_clock/-/tree/main/08_051_low_power
https://gitlab.com/flank1er/stm8l_ht1621_clock/-/tree/main/09_051_lse
- с режимами энергосбережения Low Power Wait Mode заработали тоже без вопросов, но в свойствах проекта пришлось расширять размер "RAM" секции:

Последний, финальный проект с часам и режимом энергосбережения Low Power Wait Mode:
"https://gitlab.com/flank1er/stm8l_ht1621_clock/-/tree/main/10_051_lse_clock"
- заработал только после кардинального увеличения "RAM" секции:

По энергопотреблению микроконтроллера STM8L051f3 ситуация слекдущая:

Как видно, где-то на 40 мкА микроконтроллер STM8L051F3 ест меньше, чем его более увесистый собрат.

13) Вместо заключения

Не буду скрывать, что весь код выложенный в репозитории сырой, написанный на коленке, и предоставляется без всяких гарантий. Тем не менее, часы на 051 микроконтроллере я протестировал в течении десяти дней, и они не сломались, не сбросились, и не зависли за это время. Я питал их от двух мизинчиковых(ААА) Ni-Mh аккумуляторов и собранные на макетке они выглядели так:

Здесь из деталей только микросхема микроконтроллера, дисплей, энкодер, батарейный блок и кварц. Ну и макетка. Аккумуляторам наверно лет семь, на момент начала эксперимента их суммарное напряжение было 2.42 Вольта, по прошествии десяти дней работы, напряжение опустилось до 2.38 Вольт. Выдержат ли они работу в течении полугода, пока сказать не могу, может быть через полгода будет что добавить :)