Данная статья посвящена ультраэкономичным режимам энергосбережения микроконтроллеров STM8L: "Low Power Run Mode" и "Low Power Wait Mode". Данные режимы характерны тем, что в них отключатся флеш-память, а весь код выполняется из оперативной памяти. Также отключается главный регулятор напряжения MVR, а тактирование осуществлется от низкочастотного генератора на 38 кГц или от часового кварца. За счет этого удается добится кардинального снижения энергопотребления микроконтроллера до 3-5 мкА.
На мой взгляд, данная технология ST привлекательна за счет того, что в отличии от большинства других режимов энергосбережения, микроконтроллер не находится в режиме сна подавляющую часть времени, а продолжает свою работу, пусть и на невысокой частоте, тактируясь от LSE.
В качестве примера для данных режимов энергосбережения я бы хотел рассмотреть создание часов на батарейном питании. Пусть будет две пальчиковые батареи с ресурсом работы в один год. В качестве индикатора будет использоваться шести-разрядный жидко-кристаллический индикатор (ЖКИ) на контроллере HT1621. Данный индикатор недорогой (около 200р), и он свободно продается на али.
На первый взгляд задача может показаться примитивной, но с учетом необходимости работы в режиме пониженного энергопотребления все становится не так просто. Отключая флеш-память, вы остаетесь без прерываний вообще, т.к. в stm8 таблица прерываний не может менять местоположение, она всегда располагается на флеш-памяти. В качестве альтернативы прерываниям существует механизм Event'ов, которого нет в S-линейке микроконтроллеров STM8. Кроме того, требуется рабочий цикл программы вместе с используемыми подпрограммами и функциями размещать в оперативной памяти, размер которой в микроконтроллере STM8L051F3 равен всего одному килобайту. А кроме кода программы, там еще располагаются глобальные переменные и стек. И вот так с ходу написать такой проект мало у кого получится, прежде придется порядочно посидеть над Reference Manual. А в проекте, еще используются недокументированные функции.
Из софта нам понадобится компилятор Cosmic и среда разработки STVD, т.к. у SDCC нет инструментов загрузки кода в оперативную память и компиляции кода без абсолютных адресов, т.е. строго с относительной адресацией.
Полезные материалы:
Содержание:
I. ЖК дисплей на контроллере HT1621
II. Работа с микроконтроллером STM8L151C8 в среде STVD+Cosmic
III. Энергосберегающие режимы "Low Power Run Mode" и "Low Power Wait Mode"
Данный дисплейный модуль изготовлен для использования в 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 вы еще не трогали.
После прошивки дисплей должен заработать сразу, там пойдет таймер с миллисекундами.
По упомянутому драйверу 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.
Для начала нам предстоит несколько рутинный процесс создания базового проекта в среде 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 мкА.
Чтобы кардинально снизить энергопотребление, нужно использовать один из доступных режимов снижения энергопотребления. Начнем с 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 мкА.
Теперь давайте подключим дисплей к микроконтроллеру. Схема подключения пусть будет следующей:
Далее переносим ранее написанный драйвер дисплея в наш проект. Модуль "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
Для установки времени времени в часах, мы будем использовать энкодер. Во-первый это удобно, а во-вторых, это требует от нас минимум кода, если конечно поддержка энкодеров есть в микроконтролере. Самое главное преимущество для нас - это конечно же то, что энкодер работает без прерываний.
Энкодер настраивается и работает абсолютно также как и в 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
Прежде чем браться за энергосберегающие режимы, я хотел написать программу часов, чтобы впоследствии просто адаптировать ее для работы от часового кварца. По факту, адаптировать пришлось еще много чего, но алгоритм, в целом, я написал. Попробую его доходчиво объяснить.
Мне хотелось написать вариант программы максимально приближенный для работы на 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 }
Вот последние две строчки, что выделены красным, не так тривиальны, как может показаться на первый взгляд. О них придется поговорить по-подробнее.
Режимы энергосбережения 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.
Теперь, когда мы наконец-то разобрались с теорией и дисплеем, можно уже написать программу, которая будет работать в режиме энергосбережения "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.
Для использования 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 секунду, и возникают они когда происходит перерисовка дисплея. Теоретически можно было бы уменьшить энергопотребление, если перерисовывать не весь дисплей, а только разряд разделителя.
Микроконтроллер 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;
Теперь, кода этап разработки практически окончен, можно перенести прошивку на целевой 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 ест меньше, чем его более увесистый собрат.
Не буду скрывать, что весь код выложенный в репозитории сырой, написанный на коленке, и предоставляется без всяких гарантий. Тем не менее, часы на 051 микроконтроллере я протестировал в течении десяти дней, и они не сломались, не сбросились, и не зависли за это время. Я питал их от двух мизинчиковых(ААА) Ni-Mh аккумуляторов и собранные на макетке они выглядели так:
Здесь из деталей только микросхема микроконтроллера, дисплей, энкодер, батарейный блок и кварц. Ну и макетка. Аккумуляторам наверно лет семь, на момент начала эксперимента их суммарное напряжение было 2.42 Вольта, по прошествии десяти дней работы, напряжение опустилось до 2.38 Вольт. Выдержат ли они работу в течении полугода, пока сказать не могу, может быть через полгода будет что добавить :)