Статья рассматривает вспомогательные таймеры и систему низкочастотного тактирования в микроконтроллерах STM8S. Упор делается на "чистом" программировании на Си и Ассемблере без использования сторонних библиотек.
Несмотря на то, что рассматриваются вспомогательные модули, на мой взгляд тема довольно сложная, в первую очередь из-за огромного количества "подводных камней".
Документация которая понадобится для прочтения статьи: Reference Manual STM8S - RM0016, главы: 12 (AWU), 13 (BEEP), 14 (Independet Watchdog), 15 (Window Watchdog). В качестве целевого микроконтроллера я буду использовать 20-пиновый STM8S103F3P6.
В статье используются формулы в формате MathML который поддерживается браузером Firefox, для браузеров Chrome и Opera потребуется установить одноименное расширение MathML.
Скачать полные исходники со сборочными файлам и скомпилированными прошивками, можно по ссылке в конце статьи.
Примечание от 01.09.2022г. В SDCC версии 4.2 поменялся формат передачи аргументов функций. Если раньше все аргументы передавались через стек, то теперь они передаются через регистры. Поэтому для совместимости со старым кодом следует добавлять опцию компиляции "--sdcccall 0".
Микроконтроллеры STM8 имеют три инструкции для перехода в режим энергосбережения: wfi, wfe, halt. В активный режим они возвращаются при прерывании или event'у, если используется wfe.
Имеется два основных режима энергосбережения: wait и halt. При первом отключается от тактирования только CPU, при втором отключается главная шина тактирования fMASTER и соответственно подключенные к ней CPU и вся периферия. Напомню, что в wait и active режимах периферию можно отключать индивидуально.
Режим Active-Halt является вариантом Halt режима, и отличается от оного запущенным AWU таймером. Режим имеет два варианта: с включенным и выключенным регулятором напряжения MVR (см. описание регистра CLK_ICKR)
Все вместе это дает четыре режима энергосбережения:
Система автопробуждения микроконтроллеров STM8S - это таймер который запускается только после выполнении инструкции halt, т.е. перехода в спящий режим. Срабатывание прерывания AWU приводит к пробуждению микроконтроллера, его переходу в активный режим.
Регистры которые относятся к AWU и их предустановленные значения:
Здесь, AWU_CSR - это конфигурационный регистр, AWU_APR - хранит количество тактов, которое таймер пропускает перед срабатыванием, AWU_TBR - хранит коэффициент умножения AWU_APR.
Описание регистров AWU.
Конфигурационный регистр AWU_CSR имеет три флаговых бита:
Здесь, AWUF - флаг срабатывания прерывания, AWUEN - активирует таймер (но отчет начнется только после инструкции halt), MSR используется при калибровке LSI, когда тактовая частота LSI подается на вход таймера TIM1 или TIM3.
AWU_APR хранит 6-битное значение предделителя.
AWU_TBR хранит коэффициент умножения предделителя.
Если таймер не используется, то в AWU_TBR следует записать ноль.
Далее приводится таблица временных диапазонов, которые доступны при тех или иных значениях AWU_TBR:
Хочу обратить внимание на графу допустимых значений APRDIV (последний столбец). Значение APRDIV физически не может быть больше 64, т.к. это 6-битное значение. А вот иметь значение меньше 32 ничто не мешает. В данном случае это искусственное ограничение.
Примеры расчета значений AWU_APR и AWU_TBR:
Значение 6мс лежит в интервале 4мс - 8мс, таблицы выше. Этому интервалу соответствует значение AWU_TBR=0b101 и коэффициент - 24.
Тогда значение AWU_APR рассчитывается по следующей формуле:
Из чего следует:
Значение 3с лежит в интервале 2.08с - 5.12с, таблицы выше. Этому интервалу соответствует значение AWU_TBR 0b1110 и коэффициент - 5 * 211.
Тогда значение AWU_APR рассчитывается по следующей формуле:
Из чего следует:
В данном случае нужно будет округлить результат до целого значения: 37 или 38, и записать это значение в регистр AWU_APR.
Сделаем простой пример мигалки, где задержка будет формироваться через таймер AWU. Пусть задержка будет продолжительностью в одну секунду. Тогда в AWU_TBR заносим число 12, а в AWU_APR - 62.
Текст программы получается таким:
#include <stdint.h> #include "stm8s103f.h" #define LED 5 void awu_irq(void) __interrupt(1) { __asm bres 0x50f0,#5 ; Clear AWUF bit for AWU_CSR __endasm; } int main( void ) { // Setup GPIO PB_DDR=(1<<LED); PB_CR1=(1<<LED); PB_ODR |= (1<<LED); AWU_TBR = 12; AWU_APR = 62; // =1 sec AWU_CSR = 0x10; // set AWUEN bit for AWUCSR for(;;) { PB_ODR |= (1<<LED); halt(); PB_ODR &= ~(1<<LED); halt(); } }
Теперь, чтобы получить задержку большую чем это возможно по таблице AWU_TBR, например 10 секунд, можно сделать так:
#include <stdint.h> #include "stm8s103f.h" volatile uint8_t sec; #define LED 5 void awu_irq(void) __interrupt(1) { sec++; __asm bres 0x50f0,#5 ; Clear AWUF bit for AWU_CSR __endasm; } int main( void ) { // Setup GPIO PB_DDR=(1<<LED); PB_CR1=(1<<LED); PB_ODR |= (1<<LED); AWU_TBR = 12; AWU_APR = 62; // =1 sec AWU_CSR = 0x10; // set AWUEN bit for AWUCSR for(;;) { sec=0; PB_ODR ^= (1<<LED); while (sec <10) halt(); } }
Т.к. низкочастотный генератор LSI имеет погрешность ±12.5%, осуществлять с его помощью длительные задержки может оказаться довольно проблемной задачей.
Для ее решения возможно измерение фактической частоты LSI c помощью таймеров TIM1/TIM3. Подключение осуществляется с помощью флага MSR регистра AWU_CSR:
В stm8s103f3p6 нет таймера TIM3, поэтому остается только использовать TIM1. Мне бы не хотелось сейчас разбирать работу этого таймера в режиме захвата, в целом работает он аналогично таймерам в ATMega8. Т.е. таймер будет считать сколько его тиков прошло до появления сигнала на входе, после чего он заносит это значение в регистры результата. Если TIM1 будет работать на частоте 16 МГц, то при частоте LSI равной 128 кГц, должно получиться 125 тиков.
Однако есть пара моментов в TIM1, которые важны для понимания алгоритма измерения частоты LSI.
Основной конфигурационный регистр канала в режиме захвата: TIM1_CCMRx имеет поле предделителя ICxPSC который указывает количество импульсов для осуществления захвата:
Выставив значение ICxPSC в максимальное значение равное восьми, мы получим на выходе статистическое усредненное значение периода LSI. Что собственно нам и надо. Естественно, что значение периода вырастет в 8 раз (125 * 8 = 1000), что нужно будет учитывать при расчетах.
Второй нюанс заключается в том, что регистры результата захвата нельзя обнулить, они доступны только для чтения:
Из этого следует, что каждый раз нужно будет производить ДВА измерения, и результат первого вычитать из второго, в случае если он меньше. Если же первый результат больше второго, значит произошло переполнение счетчика.
Теперь нужно будет решить, как из полученной величины рассчитать значение APRDIV. Величина которую мы получаем является периодом LSI умноженным на 8. Для частот LSI =128 кГц, и HSI/HSE =16 МГц она равняется 1000. Если величина меньше 1000, то частота LSI выше 128 кГц. Если больше 1000, то выше.
Допустим, что нужно рассчитать по измеренной величине периода LSI значение APRDIV для задержки в 1 секунду. Значение в одну секунду фактически находится на границе 12-го и 13-го диапазонов. Чтобы не перескакивать с одного на другой в зависимости от фактической частоты LSI, я предлагаю в качестве базы взять 13-й диапазон. Это избавит нас от ситуации когда значение APRDIV принимает значение больше 64.
Тогда расчетная формула будет такой:
Где Value - это измеренное значение. Число 8 х 16 х 106 раскладывается на множители: 213 х 56. Тогда:
В итоге, все сводится к простому делению.
Программа у меня получилась такой:
#include <stdint.h> #include "stm8s103f.h" #define bset(X,Y) __asm bset X,Y __endasm #define LED 5 static uint8_t lsi_measurement(); volatile uint8_t sec; void awu_irq(void) __interrupt(1) { sec++; bset(0x50f0,#5); // Clear AWUF bit for AWU_CSR } int main( void ) { // Set fCPU = 16MHz CLK_CKDIVR=0; // Setup GPIO PB_DDR=(1<<LED); PB_CR1=(1<<LED); PB_ODR |= (1<<LED); AWU_TBR = 13; AWU_APR = lsi_measurement(); // =1 sec AWU_CSR = 0x10; // set AWUEN bit for AWUCSR for(;;) { sec=0; PB_ODR ^= (1<<LED); while (sec <10) halt(); } }
Функцию lsi_measurement() я написал на ассемблере:
.module DELAY .area CODE .area HOME .include "stm8s103f.s" .globl _lsi_measurement _lsi_measurement: ; LSI measurment bset AWU_CSR, #0 ; LSI measurement enable bres TIM1_CCER1,#0 ; capture disable mov TIM1_CCMR1, #0x0d ; CC1 channel is configured as input, IC1 is mapped on TI1FP1; prescaller =8 bres TIM1_CCER1, #1 ; Capture on a rising edge of TI1F or TI2F bset TIM1_CCER1,#0 ; capture enable bset TIM1_CR1,#0 ; turn on TIM1 ; get first Value wait_capture: btjf TIM1_SR1,#1, wait_capture pushw x ld a, TIM1_CCR1H ld (01,sp),a ld a, TIM1_CCR1L ld (02,sp),a bres TIM1_SR1,#1 ; Clear CC1 flag ; get second value wait_capture_again: btjf TIM1_SR1,#1, wait_capture_again ldw y, TIM1_CCR1 bres TIM1_SR1, #1 ; Clear CC1 flag bres TIM1_CCER1, #0 ; Capture disable bres TIM1_CR1, #0 ; turn off TIM1 bres AWU_CSR, #0 ; LSI measurement disable subw y,(01,sp) ; Value2 - Value1 popw x tnzw y jrmi _lsi_measurement ; if result is negative, try again ldw x, #31250 divw x,y ld a,xl ret
Систему AWU возможно тактировать внешнего кварца или генератора HSE. Предделитель понижает частоту кварца до 128 кГц, при этом не обязательно fMASTER переключать на HSE. Хотя смысла в том, что бы использовать HSI при наличии кварца я не вижу.
Использовать можно кварцы на 16, 8 и 4 МГц. Включается все это хозяйство через Option Bytes, флаги CKAWUSEL и PRSC:
Тестовый пример:
#include <stdint.h> #include "stm8s103f.h" volatile uint8_t sec; #define LED 5 void awu_irq(void) __interrupt(1) { sec++; __asm bres 0x50f0,#5 ; Clear AWUF bit for AWU_CSR __endasm; } int main( void ) { // Setup GPIO PB_DDR=(1<<LED); PB_CR1=(1<<LED); AWU_TBR = 12; AWU_APR = 62; // =1 sec AWU_CSR = 0x10; // set AWUEN bit for AWUCSR for(;;) { sec=0; PB_ODR ^= (1<<LED); while (sec <10) halt(); } }
Модуль бипера также тактируется от низкочастотного LSI. Он имеет всего один конфигурационный регистр BEEP_CSR:
Пример программы:
#include <stdint.h> #include "stm8s103f.h" #define bset(X,Y) __asm bset X,Y __endasm #define LED 5 volatile uint8_t sec; void awu_irq(void) __interrupt(1) { sec++; bset(0x50f0,#5); // Clear AWUF bit for AWU_CSR } int main( void ) { // Setup GPIO PB_DDR=(1<<LED); PB_CR1=(1<<LED); AWU_TBR = 12; AWU_APR = 62; // =1 sec AWU_CSR = 0x10; // set AWUEN bit for AWUCSR // BEEP_CSR=0x9e; // peeeeeck... (high tone) BEEP_CSR=0x1e; // buuuuuuuuuzzzzz (low tone) bset(0x50f3,#5); // Enable BEEP for(;;) { sec=0; PB_ODR ^= (1<<LED); while (sec <10) halt(); } }
Сторожевой таймер - это штука которая избавляет нашу программу от лишней логики в постоянных проверках того или другого. Самый распространенный случай - проверка таймаута. При настроенном сторожевом таймере, можно не боятся зацикливания программы при какой-либо ошибке. При возникновении проблем, он перезагрузит микроконтроллер, и программа начнет выполняться заново.
Интервал сторожевого таймера можно настроить в пределах от нескольких микросекунд до одной секунды. Т.к. сторожевой таймер тактируется от LSI, на все интервалы должна распространяться погрешность генератора ±12.5%.
Сторожевой таймер имеет три регистра и систему защиты от случайного изменения при дребезге питания или при непредсказуемом поведении (undefined behaviour).
Функционально устройство сторожевого таймера показано на следующей схеме:
Интервал сторожевого таймера рассчитывается по формуле:
Сторожевой таймер имеет три регистра: регистр доступа - IWDG_KR, регистр предделителя - IWDG_PR, и регистр счетчика - IWDG_RLR
Прежде чем что-то записать в последние два регистра, сначала следует открыть к ним доступ записью числа 0x55 в регистр IWDG_KR. При записи в IWDG_KR числа 0xAA, закрывается доступ на запись регистров IWDG_PR и IWDG_RLR, их содержимое при этом обновляется. Если при закрытом доступе к IWDG_PR и IWDG_RLR записать в IWDG_KR число 0xAA, содержимое регистра счетчика - IWDG_RLR восстановится.
Описание регистра доступа IWDG_KR:
Регистр предделителя IWDG_PR:
Счетчик IWDG_RLR:
Пример программы:
#include <stdint.h> #include "stm8s103f.h" #define bset(X,Y) __asm bset X,Y __endasm #define bres(X,Y) __asm bres X,Y __endasm #define LED 5 extern void delay(uint16_t value); int main( void ) { // Setup GPIO PB_DDR=(1<<LED); PB_CR1=(1<<LED); // Setup Buzzer BEEP_CSR=0x1e; // buuuuuuuuuzzzzz (low tone) bset(0x50f3,#5); // Enable BEEP delay(100); bres(0x50f3,#5); // Watchdog Setup IWDG_KR=0xcc; IWDG_KR=0x55; // unlock IWDG_PR & IWDG_RLR IWDG_PR=0x6; // =256 IWDG_RLR=0xff; // 1.024 sec IWDG_KR=0xaa; // lock IWDG_PR & IWDG_RLR // main loop for(;;) { PB_ODR |= (1<<LED); delay(500); IWDG_KR=0xaa; delay(500); IWDG_KR=0xaa; PB_ODR &= ~(1<<LED); delay(500); IWDG_KR=0xaa; delay(500); IWDG_KR=0xaa; delay(800); IWDG_KR=0xaa; delay(500); IWDG_KR=0xaa; } }
Здесь при включении микроконтроллера пьезодинамик издает короткий сигнал, а мигающий светодиод имитирует рабочий цикл. Если в аргументах функции delay() поставить аргумент больше 1000, то пьезодинамик начнет постоянно пищать.
Для контроля таймаутов в STM8 имеется специальный оконный сторожевой таймер, который следит за тем, чтобы интервал был не более и не менее заданного времени.
ВНИМАНИЕ! Я должен предупредить, что на моем STM8S103F3P6 этот таймер работал немого не так как описано в документации. Это означает, что на другом микроконтроллере или ревизии его поведение возможно будет более "каноническим".
Ок. Таймер Имеет два регистра: WWDG_CR и WWDG_WR:
Таймер тактируется от высокочастотных HSI/HSE с фиксированным предделителем равным 12288. При этом, счетчик таймера 5-битный, .т.е. диапазон его значений: 0-63.
Таймер продолжает работать после инструкции wfi и останавливается после halt. Через Option Bytes можно задать чтобы после halt микроконтроллер перезагружался.
Счетчик таймера задается в конфигурационном регистре WWDG_CR, биты 0-5. Старший, 7-й бит управляет состоянием таймера: включено/выключено. Сброс 6-го бита регистра при включенном таймере приводит к перезагрузке микроконтроллера. Счетчик вычитающий.
Оконный регистр WWDG_WR задает минимальное время работы таймера. Если счетчик WWDG_CR был обновлен при значении большем чем содержится в WWDG_WG, микроконтроллер будет перезагружен.
Следующий рисунок демонстрирует задание временного окна сторожевого таймера:
Далее показаны минимальные и максимальные временные интервалы которые можно получить при частотах HSI равных 2 и 16 МГц:
Я приведу несколько примеров использования оконного сторожевого таймера, которые демонстрируют его фактическую работу в stm8s103f3p6. Она, немного отличается от своего описания в документации.
Допустим нужно задать временную границу таймаута, после которого ждать уже бессмысленно, и следует осуществить перезагрузку микроконтроллера:
#include <stdint.h> #include "stm8s103f.h" #define bset(X,Y) __asm bset X,Y __endasm #define bres(X,Y) __asm bres X,Y __endasm #define LED 5 extern void delay(uint16_t value); int main( void ) { // Setup GPIO PB_DDR=(1<<LED); PB_CR1=(1<<LED); PB_ODR |=(1<<LED); // Setup Buzzer BEEP_CSR=0x1e; // buuuuuuuuuzzzzz (low tone) bset(0x50f3,#5); // Enable BEEP delay(100); bres(0x50f3,#5); // Watchdog Setup WWDG_WR=0x40; // max value WWDG_CR=0x7f; // max value bset(0x50d1,#7); // start window watchdog // main loop for(;;) { PB_ODR ^= (1<<LED); delay(310); WWDG_CR|=0x7f; } }
Здесь, если в главном цикле указать задержку delay(350) или более, то микроконтроллер уже начнет перезагружаться. Задержку можно варьировать от 1 до примерно 310(подбирается экспериментально). Частота HSI - 2 МГц.
Вначале программы счетчику присваивается максимальное значение 0x7f. В процессе работы он уменьшается, и если достигает значения 0x3f, то микроконтроллер перезагружается.
Теперь, допустим, нужно сделать контроль минимального времени задержки. Если в первом примере закоментировать строку delay(310), и перепрошить микроконтроллер, пьезодинамик начнет непрерывно пищать.
И тут на помощь приходит оконный регистр. Если выставить WWDG_WR в значение 0x7f, писк прекратиться. Оконный таймер перестанет реагировать на минимальный интервал. Если WWDG_WR выставить в значение равное 0x7e, писк снова возобновится.
В следующем примере, если выставить границу цикла не в 100, а скажем в 90 или меньше, пьезодинамик начнет пищать:
#include <stdint.h> #include "stm8s103f.h" #define bset(X,Y) __asm bset X,Y __endasm #define bres(X,Y) __asm bres X,Y __endasm #define LED 5 extern void delay(uint16_t value); int main( void ) { // Setup GPIO PB_DDR=(1<<LED); PB_CR1=(1<<LED); PB_ODR |=(1<<LED); // Setup Buzzer BEEP_CSR=0x1e; // buuuuuuuuuzzzzz (low tone) bset(0x50f3,#5); // Enable BEEP delay(100); bres(0x50f3,#5); // Watchdog Setup WWDG_WR=0x7e; // min value WWDG_CR=0x7f; // max value bset(0x50d1,#7); // start window watchdog // main loop for(;;) { uint16_t i; PB_ODR ^= (1<<LED); for(i=0;i<0x100;i++); WWDG_CR|=0x7f; } }
Данный пример подходит для выставления минимального времени срабатывания оконного таймера, но как "двигать" эту границу я не знаю. Фактически, это можно только включить или отключить. Значение 0x7e регистра WWDG_WR, можно поменять на любое меньшее значения регистра WWDG_CR.
Т.о. чтобы включить перезагрузку микроконтроллер по недобору минимального времени в интервале, значение регистра WWDG_WR должно быть на единицу меньше значения WWDG_CR.
Если ждать 300 мс нет необходимости, то максимальное время задержки можно сократить до 4 мс:
#include <stdint.h> #include "stm8s103f.h" #define bset(X,Y) __asm bset X,Y __endasm #define bres(X,Y) __asm bres X,Y __endasm #define LED 5 extern void delay(uint16_t value); int main( void ) { // Setup GPIO PB_DDR=(1<<LED); PB_CR1=(1<<LED); PB_ODR |=(1<<LED); // Setup Buzzer BEEP_CSR=0x1e; // buuuuuuuuuzzzzz (low tone) bset(0x50f3,#5); // Enable BEEP delay(100); bres(0x50f3,#5); // Watchdog Setup WWDG_WR=0x3f; // min value WWDG_CR=0x40; // max value bset(0x50d1,#7); // start window watchdog // main loop for(;;) { uint16_t i; PB_ODR ^= (1<<LED); // for(i=0;i<0x100;i++); delay(4); WWDG_CR|=0x40; } }
Здесь, если значение delay() выставить больше 4-х, то микроконтроллер будет перезагружаться по срабатыванию сторожевого таймера.
В завершении напомню, что при изменении частоты fMASTER, соответственно поменяются и тайминги.
Скачать полные исходники со сборочными файлам и скомпилированными прошивками, можно по этой ссылке .