Зеркало сайта: vivacious-stockings-frog.cyclic.app

STM8S105 + COSMIC: Запись в EEPROM и FLASH память микроконтроллера

разделы: STM8 , дата: 21 сентября 2018г.

Когда я писал драйвер FM-модуля RDA5807, то у меня возникла необходимость сохранять куда-то найденные станции, чтобы потом можно было переключиться на них одной кнопкой, минуя поиск. Но когда я полез в документацию STM8, чтобы поискать, как это можно было бы осуществить, то понял, что EEPROM и FLASH в STM8 - это отдельная подсистема микроконтроллера, и изучать ее надо всю.

Один из режимов записи в EEPROM/FLASH требует выполнения из ОЗУ. Вопрос копирования кода в ОЗУ и выполнения его оттуда я затрагивал в предыдущей статье, однако там вся реализация была на ассемблере. Сейчас же мне захотелось показать как это делается в Си.

В качестве компилятора я выбрал COSMIC, по которому уже как-то писал быстрый старт. Но тогда я писал об использованию COSMIC совместно с SPL библиотекой. На этот раз мне хочется раскрыть тему программирования в COSMIC, используя "чистый" Си в связке с ассемблером. Правда должен оговориться, что несколько отредактированные загловочные файлы из SPL в этой статье я все-таки использовать буду, т.к. нужны будут именновые константы масок периферийных регистров.

В итоге статья получилось составленной из двух взаимосвязанных тем: сначала рассматривается вопрос использования компилятора COSMIC, а затем, как с его помощью сохранять данные в EEPROM/FLASH памяти микроконтроллера.

В качестве Develop Board я буду использовать собственную плату с чипом STM8S105C4T6. Это Medium-Density чип с 16 КБ флеш-памяти, 2 КБ ОЗУ и 1 КБ ЭСППЗУ(EEPROM). Он более интересен чем STM8S103-й чип, т.к. в 105-ом имеется встроенный загрузчик(bootloader), механизм read-while-write (RWW), а размер блока составляет 128 байт вместо 64 байт на 103-м чипе. Вы в свою очередь можете использовать фирменную отладочную плату STM8S-DISCOVERY с чипом STM8S105C6T6. Там флеш-памяти будет побольше - 32 КБ. На худой конец, можно воспользоваться ещё одной китайской платой на 105-м чипе. Также как в STM8S-DISCOVERY в ней установлен кварц на 8 МГц. Сама плата выполнена в форм-факторе удобном для установки в беспаячную макету.

Cosmic у меня работает в связке с STVD, обе программы установлены на виртуалку, которая в свою очередь установлена в Linux. Гостевой ОС в виртуалке служит Windows XP SP3. О превратностях установки Cosmic я уже писал в вышеупомянутой статье два года назад. К сожалению, я тогда я не упомянул, что получить регистрационный ключ можно онлайн. Т.е. не надо ждать несколько дней чтобы ключ скинули на e-mail, как было в моем случае. Если не ошибаюсь, ключ действует один год, и по истечении регистрационного периода, его нужно получать заново. Кроме того, ключ "слетает" при копировании виртуальной машины. В этом случае его также следует получать по новой. В последнем случае я просто удалял Cosmic и затем ставил его заново, получая свежий ключ. Сейчас у меня следующая версия компилятора:

COSMIC Software STM8 C Cross Compiler (Special Edition) V4.4.7

Так же как и в предыдущей статье, для контроля кода прошивки я буду использовать дизассемблер из комплекта утилит stm8-binutils.

    Список используемой документации:
  1. Cosmic CXST7 - User Manual - документация по компилятору COSMIC
  2. Reference Manual STM8S - RM0016, глава 4 "Flash program memory and data EEPROM"
  3. Programming Manual PM0051 - руководство по записи в EEPROM и FLASH память для микроконтроллеров STM8S/STM8A
  4. Application note - AN2659, - глава пятая - "Configuring the Cosmic compiler for RAM code execution".
  5. Работа с EEPROM и Flash / STM8 / Сообщество EasyElectronics.ru хорошая вводная статья по теме на русском языке. Написано по существу, без лишней воды.

Содержание статьи:

    I. Основы работы со связкой COSMIC + STVD
  1. Создание базового проекта в среде разработки STVD+COSMIC
  2. Добавление ассемблерного файла к проекту
  3. Добавление ассемблерных обработчиков прерываний
  4. Маппинг на физические адреса
    II. Основы работы с EEPROM/FLASH подсистемой в микроконтроллерах STM8
  1. Особенности EEPROM/FLASH подсистемы в микроконтроллерах STM8
  2. Регистры подсистемы EEPROM/FLASH
  3. Запись в EEPROM средствами COSMIC
  4. Безопасное снятие защиты MASS, однобайтный режим записи в EEPROM/FLASH
  5. Четырехбайтный режим записи в EEPROM/FLASH
  6. Блоковый режим записи в EEPROM/FLASH. Копирование кода в ОЗУ и выполнение его оттуда средствами COSMIC

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

1 Создание базового проекта в среде разработки STVD+COSMIC

В STVD откроем новый Си проект с использованием тулчейна COSMIC. Выглядеть он будет так:

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

Здесь s19 и elf - бинарные файлы с итоговой прошивкой. Файлы с расширением "o" - это объектные файлы. lkf - это скрипт компоновщика.

Заглянем например в 01_blink.lkf:

# LINK COMMAND FILE AUTOMATICALLY GENERATED BY STVD:
#  * TOTALLY IF AUTO MODE IS ENABLED
#  * ONLY INSIDE MARKERS IN SEMI-AUTO MODE
#
# CHOOSE THE CUSTOM MODE IN STVD IN ORDER TO HAVE FULL CONTROL OVER THIS FILE.
#
# Sections delimited by  and  markers are reserved for
# STVD: DO NOT MODIFY INSIDE.
#
# Manual modifications are allowed OUTSIDE these sections, WHEN STVD AUTO MODE
# IS DISABLED.
#
# CAUTION:
#  * Changing from Custom to Semi-Auto mode removes modifications in
#    STVD-reserved sections
#  * Changing to Auto mode removes all modifications.
#
# In Semi-Auto mode, you are allowed to remove  and  markers
# in order to gain control over the concerned sections. As a consequence any
# modification from the STVD graphical interface concerning this section will be
# ignored.
#
# Please refer to Cosmic User Manuals before any modification.
# Note that errors in editing this file may have unpredictable results when
# running STVD.

# Segment configuration - section reserved for STVD
#
# Segment Code,Constants:
+seg .const -b 0x8080 -m 0x3f80  -n .const -it 
+seg .text -a .const  -n .text 
# Segment Eeprom:
+seg .eeprom -b 0x4000 -m 0x400  -n .eeprom 
# Segment Zero Page:
+seg .bsct -b 0x0 -m 0x100  -n .bsct 
+seg .ubsct -a .bsct  -n .ubsct 
+seg .bit -a .ubsct  -n .bit -id 
+seg .share -a .bit  -n .share -is 
# Segment Ram:
+seg .data -b 0x100 -m 0x500  -n .data 
+seg .bss -a .data  -n .bss 
#


# Startup file - section reserved for STVD
#
crtsi0.sm8
#


# Object files list - section reserved for STVD
#
Debug\main.o
#


# Library list - section reserved for STVD
#
libis0.sm8
libm0.sm8
#


# Interrupt vectors file - section reserved for STVD
#
+seg .const -b 0x8000 -k
Debug\stm8_interrupt_vector.o
#

# Defines - section reserved for STVD
#
+def __endzp=@.ubsct			# end of uninitialized zpage
+def __memory=@.bss				# end of bss segment
+def __startmem=@.bss
+def __endmem=0x5ff
+def __stack=0x7ff
#

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

Технически, мы можем использовать COSMIC без STVD, программировать в командной строке, писать Makefile'ы и пр. Например составим такой main.c

void main(void) {
    while(1);
}

Скомпилируем его в объектный файл командой:

$ cxstm8.exe +mods  -v  main.c
main.c:
        "cpstm8" -o "C:\cygwin\tmp\smc.cx1" -i "C:\Program Files\COSMIC\FSE_Compilers\CXSTM8\HSTM8" -u -pb -hmods.h -d"__VERS__=\"V4.4.7\"" "main.c"
        "cgstm8" -o "C:\cygwin\tmp\smc.cx2" -fl "C:\cygwin\tmp\smc.cx1"
        "costm8" -o "C:\cygwin\tmp\smc.cx1" "C:\cygwin\tmp\smc.cx2"
        "castm8" -o "main.o" -i "C:\Program Files\COSMIC\FSE_Compilers\CXSTM8\HSTM8" "C:\cygwin\tmp\smc.cx1"

На выходе мы получим объектный файл main.o. Теперь составим скрипт компоновщика main.lkf следующего вида:

## main.lkf
+seg .text -b0x8000
+seg .data -b0x4000
main.o

линкуем:

$ clnk.exe -o main.sm8 -v main.lkf
- main.o
main.o:

В этот раз на выходе получаем main.sm8. Следующим шагом будет получение elf-файла:

$ cvdwarf.exe -o main.elf -v main.sm8
Reading Debug Symbols ..
Outputing HEADER PART
Outputing PROGRAM HEADER PART
Outputing IMAGE LOAD PART
Outputing SYMBOLS TABLE PART
Outputing IMAGE DEBUG PART
Outputing LINES INFO
Outputing DEBUG INFO
Outputing DEBUG ABBREV
Outputing DEBUG LOCATION
Outputing DEBUG FRAME
Outputing STRING TABLE PART
Outputing SECTION NAME TABLE PART
Outputing SECTIONS TABLE PART

Смотрим дизассемблером что у нас получилось:

$ stm8-objdump.exe -m stm8 -S ./main.elf

./main.elf:     формат файла elf32-big


Дизассемблирование секции .text:

00008000 <.text>:
    8000:       20 fe           jra 0x8000 ;0x8000

Таким образом работает компилятор COSMIC в backend режиме STVD. Сегодня я не собираюсь что-то делать в таком стиле, но на мой взгляд иметь представление об этапах компиляции полезно.

Возвращаясь с списку сгенерированых файлов проекта в STVD, хочу обратить ещё внимание на файлы с расширением ls. В них помещается ассемблерный вариант программы:

   1                     ; C Compiler for STM8 (COSMIC Software)
   2                     ; Parser V4.11.10 - 06 Jul 2017
   3                     ; Generator (Limited) V4.4.7 - 05 Oct 2017
2550                     ; 4 main()
2550                     ; 5 {
2552                     	switch	.text
2553  0000               _main:
2557  0000               L1461:
2558                     ; 6 	while (1);
2560  0000 20fe          	jra	L1461
2573                     	xdef	_main
2592                     	end

У вас есть выбор: пользоваться дизассемблером или поглядывать в эти файлы для контроля компиляции.

Ок. Начинаем писать базовый проект на COSMIC. В начале файла main.c добавляем следующие заголовочные файлы:

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

Первый заголовочный файл добавляет целочисленные типы uint8_t, uint16_t и прочее. Второй заголовочный файл добавляет адреса регистров периферийных устройств. Путь к заголовочным файлам заедается в свойствах проекта:

Ниже представлены заголовочные файлы которые идут вместе с компилятором COSMIC:

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

#define BLUE_LED 3      // PA3
#define GREEN_LED 7     // PB7

static void delay(uint16_t t) {
    while(t--);
}

Главная функция main() не будет отличаться оригинальностью:

main()
{
    // ------- CLK Setup ---------------
    CLK_CKDIVR = 0;         // fCPU=16MHz
    // ------- GPIO setup --------------
    // BLUE LED
    PA_DDR |= (1<<BLUE_LED);
    PA_CR1 |= (1<<BLUE_LED);
    // GREEN LED
    PB_DDR |= (1<<GREEN_LED);
    PB_CR1 |= (1<<GREEN_LED);

    for(;;) {
        PA_ODR ^=(1<<BLUE_LED);
        delay(0xffff);
    }
}

Компилируем, прошиваем. Если все прошло успешно, то в дизассемблерном виде программа будет выглядеть так:

delay:
    pushw X
while:
    ldw X,(0x01,SP) ;0x1
    subw X,#0x0001 ;0x1
    ldw (0x01,SP),X ;0x1
    addw X,#0x0001 ;0x1
    cpw X,#0x0000
    jrne while ;0x80d4
    popw X
    ret
main:
    clr _CLK_CKDIVR
    bset _PA_DDR,#3
    bset _PA_CR1,#3
    bset _PB_DDR,#7
    bset _PB_CR1,#7
loop:
    bcpl _PA_ODR,#3
    ldw X,#0x03e8 ;3e8 <__memory+0x2e8>
    callr _delay
    jra loop

Можно видеть, что параметр функции передаётся через X регистр, а не через стек как в SDCC.

Я хочу обратить внимание на то, что для вызова функции delay() используется инструкция вызова подпрограммы по относительному адресу - CALLR. Если мы зайдём в настройки проекта и откроем вкладку настройки компилятора, то в выпадающем списке сможем настроить используемую модель памяти.

По умолчанию используется Short Stack (+mods0), когда по мере возможности используются относительные вызовы CALLR, а в стек помещается один байт в качестве адреса возврата. Если поменять модель использования памяти на Long Stack (+modsl0), то по умолчанию, для вызова подпрограмм будет использоваться инструкция CALL, и в стек будет помещаться двухбайтный адрес возврата. Если же переключиться на модель Long Stack (+modsl), то забегая вперёд, метка вызываемой функции должна будет иметь префикс f_ вместо простого символа подчёркивания. Во-вторых, вместо инструкции CALL будет использоваться расширенный вызов CALLF с 3-х байтным адресом.

В модели использования памяти Short Stack (+mods0), внешние функции из других модулей которые мы в дальнейшем будем добавлять, будут вызываться через "обычную" инструкцию вызова CALL.

2 Добавление ассемблерного файла в проект

Теперь заменим функцию задержки на ассемблерный вариант. Для этого добавим в проект файл с именем, допустим: "asm.s", в который будем складывать ассемблерные функции и обработчики прерываний на ассемблере.

Синтаксис ассемблера в COSMIC самый что ни на есть классический, т.е. после метки должно идти двоеточие, шестнадцатеричное число начинается с префикса "0x"(не совсем канон, да), а комментарий должен начинаться с точки с запятой. В макросы я вдаваться не хочу, т.к. предполагаю, что ассемблер будет использоваться только для самого необходимого.

Содержимое asm.s пусть пока будет таким:

    switch .text
    ; ------------------
    xref _delay
_delay:
    pushw x
    pushw y
start_delay:
    ldw y, #3960      ; ->((500-5)=495)*8 (16MHz)
loop:
    subw y,#1
    jrne loop
    decw x
    jrne start_delay
    popw y
    popw x
    ret

    end

Если функция вызывается из Си-программы, то метка должна начинаться с символа подчёркивания. Директива xref делает метку видимой компоновщику, а директива switch указывает рабочий сегмент.

В main.c заменим функцию delay объявлением внешней функции:

extern void delay(uint16_t value);

В главном цикле, в параметре вызова функции delay(), заменим абстрактное число 0xffff на 1000, т.е. на одну секунду.

Если в функции указать более одного параметра, то первый будет по прежнему помещён в Х регистр, а второй будет передан через стек.

Хочу обратить внимание, что если дизассмблировать прошивку с флагом "S", то мы увидим следующую картинку:

 stm8-objdump.exe -m stm8 -S ~/docs/article_eeprom/01_blink/Debug/01_blink.elf

/home/flanker/docs/article_eeprom/01_blink/Debug/01_blink.elf:     формат файла elf32-big


Дизассемблирование раздела .text:

00008083 <__stext>:
    8083:       ae 07 ff 94 90 ce 80 80 ae 80 82 f6 27 25 a5 60     ............'%.`
    8093:       27 17 bf 00 ee 03 bf 03 be 00 ee 01 90 f6 f7 5c     '..............\
    80a3:       90 5c 90 b3 03 26 f5 be 00 90 93 90 ee 03 1c 00     .\...&..........
    80b3:       05 20 d8 ae 00 00 20 02 f7 5c a3 00 06 26 f9 ae     . .... ..\...&..
    80c3:       01 00 20 02 f7 5c a3 01 00 26 f9 cd 80 e7           .. ..\...&....

000080d1 <_exit>:
    80d1:       20 fe                                                .

000080d3 <_delay>:
    80d3:       89              pushw X
    80d4:       90 89           pushw Y
    80d6:       90 ae 0f 78     ldw Y,#0x0f78 ;f78 <__stack+0x779>
    80da:       72 a2 00 01     subw Y,#0x0001 ;1 <c_x+0x1>
    80de:       26 fa           jrne 0x80da ;80da <_delay+0x7>
    80e0:       5a              decw X
    80e1:       26 f3           jrne 0x80d6 ;80d6 <_delay+0x3>
    80e3:       90 85           popw Y
    80e5:       85              popw X
    80e6:       81              ret

000080e7 <_main>:
    80e7:       72 5f 50 c6     clr _CLK_CKDIVR
    80eb:       72 16 50 02     bset _PA_DDR,#3
    80ef:       72 16 50 03     bset _PA_CR1,#3
    80f3:       72 1e 50 07     bset _PB_DDR,#7
    80f7:       72 1e 50 08     bset _PB_CR1,#7
    80fb:       90 16 50 00     bcpl _PA_ODR,#3
    80ff:       ae 03 e8        ldw X,#0x03e8 ;3e8 <__memory+0x2e8>
    8102:       cd 80 d3        call _delay
    8105:       20 f4           jra 0x80fb ;80fb <_main+0x14>

00008107 <f_NonHandledInterrupt>:
    8107:       80              iret

Дизассемблирование раздела .const:

00008000 <__vectab>:
    8000:       82 00 80 83 82 00 81 07 82 00 81 07 82 00 81 07     ................
    8010:       82 00 81 07 82 00 81 07 82 00 81 07 82 00 81 07     ................
    8020:       82 00 81 07 82 00 81 07 82 00 81 07 82 00 81 07     ................
    8030:       82 00 81 07 82 00 81 07 82 00 81 07 82 00 81 07     ................
    8040:       82 00 81 07 82 00 81 07 82 00 81 07 82 00 81 07     ................
    8050:       82 00 81 07 82 00 81 07 82 00 81 07 82 00 81 07     ................
    8060:       82 00 81 07 82 00 81 07 82 00 81 07 82 00 81 07     ................
    8070:       82 00 81 07 82 00 81 07 82 00 81 07 82 00 81 07     ................

Дизассемблирование раздела .init:

00008080 <__idesc__>:
    8080:       80 83 00

Здесь нам дизассеблировали только тот код который мы сами написали. Таблица векторов и библиотеки COSMIC отображались массивом данных, т.к. там нет отладочной информации. Можно использовать флаг D вместо S для полного дизассемблирования, но тогда на выходе получим "портянку" без указания периферийных регистров и сегментов.

3 Добавление ассемблерных обработчиков прерываний

Теперь займёмся прерываниями. Предполагаем, что обработчики прерываний мы будем писать на ассемблере. Тогда первое за что следует взяться, это файл по умолчанию входящий в шаблон проекта: "stm8_interrupt_vector.c"

Ассемблер COSMIC также как ST-Assembler не знает о существовании инструкции INT, поэтому таблица векторов задана в виде массива констант: опкода инструкции INT, плюс адреса обработчика прерывания. Для удобства, я добавил комментарии к таблице:

struct interrupt_vector const _vectab[] = {
    {0x82, (interrupt_handler_t)_stext}, // Reset
    {0x82, NonHandledInterrupt}, // Trap - Software Interrupt
    {0x82, NonHandledInterrupt}, // int0 TLI - External Top Level Interrupt
    {0x82, NonHandledInterrupt}, // int1 AWU - Auto Wake Up From Halt
    {0x82, NonHandledInterrupt}, // int2 CLK - Clock Contoller
    {0x82, NonHandledInterrupt}, // int3 EXTI0 - Port A External Interrupts
    {0x82, NonHandledInterrupt}, // int4 EXTI1 - Port B External Interrupts
    {0x82, NonHandledInterrupt}, // int5 EXTI2 - Port C External Interrupts
    {0x82, NonHandledInterrupt}, // int6 EXTI3 - Port D External Interrupts
    {0x82, NonHandledInterrupt}, // int7 EXTI4 - Port E External Interrupts
    {0x82, NonHandledInterrupt}, // int8 - Reserved
    {0x82, NonHandledInterrupt}, // int9 - Reserved
    {0x82, NonHandledInterrupt}, // int10 SPI  - End Of Tranfer
    {0x82, NonHandledInterrupt}, // int11 TIM1 - Update/Overflow/Underflow/trigger/break
    {0x82, NonHandledInterrupt}, // int12 TIM1 - Capure/Compare
    {0x82, NonHandledInterrupt}, // int13 TIM2 - Update/Overflow
    {0x82, NonHandledInterrupt}, // int14 TIM2 - Capure/Compare
    {0x82, NonHandledInterrupt}, // int15 - Reserved
    {0x82, NonHandledInterrupt}, // int16 - Reserved
    {0x82, NonHandledInterrupt}, // int17 UART1 - Tx Complete
    {0x82, NonHandledInterrupt}, // int18 UART1 - Reveive Register DATA FULL
    {0x82, NonHandledInterrupt}, // int19 I2C - Interrupt
    {0x82, NonHandledInterrupt}, // int20 - Reserved
    {0x82, NonHandledInterrupt}, // int21 - Reserved
    {0x82, NonHandledInterrupt}, // int22 ADC1 - End Of Conversion/Analog Watchdog Interrupt
    {0x82, NonHandledInterrupt}, // int23 TIM4 - Update/Overflow
    {0x82, NonHandledInterrupt}, // int24 Flash - EOP/WR_PG_DIS
    {0x82, NonHandledInterrupt}, // int25 - Reserved
    {0x82, NonHandledInterrupt}, // int26 - Reserved
    {0x82, NonHandledInterrupt}, // int27 - Reserved
    {0x82, NonHandledInterrupt}, // int28 - Reserved
    {0x82, NonHandledInterrupt}, // int29 - Reserved
};

В обработчик пустого прерывания NonHandledInterrupt я вставил строку:

    while(1);

Не могу сказать сколько раз меня это выручало. Если во время отладки перестаёт мигать светодиод главного цикла, значит я снова забыл вставить обработчик прерывания в таблицу векторов.

В качестве примера добавим функцию задержки на таймере TIM4. Тема не нова и была не раз уже разобрана: Задержка по таймеру TIM4, с обработчиком прерывания на ассемблере, STM8+SDCC+SPL: функции delay_ms() и delay_us() на таймере TIM4. Так что дело за техникой.

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

Также в каталог проекта нужно добавить заголовочный файл tim4.h с объявлением функции delay_ms(uint16_t ms):

#ifndef __TIM4.H__
#define __TIM4.H__
#include <stdint.h>

void delay_ms(uint16_t ms);

#endif

После этого, добавим в проект файл: "tim4.c" следующего содержания:

#include "iostm8s105.h"
#include "stm8s_tim4.h"
#include "tim4.h"

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

    TIM4_CR1  = 0x0;                        //  disable counter
}

Далее, в ассемблерный файл "asm.s" добавим обработчик прерывания таймера TIM4:

    ; --- TIM4 HANDLER -----
    xdef _tim4_handler
_tim4_handler:
    bres TIM4_SR,#0     ; Clear pending flag
    iret 

Во-первых, обращаю внимание, что ассемблерные обработчики прерываний объявляются через директиву xdef, тогда как ассемблерные функции через директиву xref.

Во-вторых, здесь используется мнемоника для регистра TIM4_SR, что означает, что в проект нужно добавить ещё один файл, на этот раз ассемблерный, с адресами периферийных регистров. С помощью потокового редактора sed, я конвертировал файл: "STM8S105C_S.h" из комплекта STVD в нужный формат. Скачать его можно здесь. Файл следует добавить в каталог проекта, после чего в начале "asm.s" вставить объявление: #include "stm8s105c_s.inc".

Теперь в начало файла: "stm8_interrupt_vector.c" добавим объявление внешнего обработчика:

extern void tim4_handler();

После чего, у 23-го прерывания поменяем обработчик "NonHandledInterrupt" на наш "(interrupt_handler_t)tim4_handler".

Осталось в main.c добавить объявление заголовочного файла tim4.h, перед главным циклом разрешить прерывания командой: _asm("rim"), и после, в главном цикле можно менять вызов функции delay(1000) на delay_ms(1000).

У этого простого примера есть интересный нюанс. Если из функции main() убрать инструкцию разрешения прерывания - "rim", то прерывание все-равно будет работать. Судя по отладчику, разрешение прерываний происходит после выполнения инструкции iret. При этом, каким-то образом этот обработчик вызывается. Могу предположить, что инструкция wfi разрешает прерывания.

4 Маппинг на физические адреса

В COSMIC есть способ привязать переменные к реальным физическим адресам. Делается это так:

char foo[10] @0x120;

Здесь массив foo будет занимать адреса с 0х120 по 0х129 включительно.

Таким образом можно маппить не только адреса в ОЗУ но и адреса периферийных регистров. В документации настаивают, что бы при маппинге на регистры ввода-вывода, переменные объявлялись с модификатором volatile:

volatile uint8_t Port_DDRA @0x5002;

Переходя ближе к теме скажу, что с помощью модификатора @eeprom можно назначать переменные в области EEPROM-памяти. Примеры:

@eeprom uint8_t foo;
@eeprom uint8_t bar[10] @0x4010;

Маппить можно не только адреса но и отдельные биты. В COSMIC есть встоенный булевый тип: _Bool. С его помощью можно сделать например так:

volatile _Bool PA4 @0x5002:4

Приведу реальный пример. Для этого в каталог проекта нужно будет добавить ещё один заголовочный файл из SPL stm8s_clk.h. Далее в начале функции main() заменим строку:

    // ------- CLK Setup ---------------
    CLK_CKDIVR = 0;         // fCPU=16MHz

на код переключения на внешний кварц:

    // ------- CLK Setup ---------------
    CLK_CKDIVR = CLK_PRESCALER_HSIDIV8;     // =0x18, set HSI prescaler =8
    // turn off all  peripherals
    CLK_PCKENR1 = 0;
    CLK_PCKENR2 = 0;
    CLK_PCKENR1 |= CLK_PCKENR1_TIM4;        // =bit4, enable TIM4
    CLK_PCKENR2 |= CLK_PCKENR2_AWU;         // =bit2, enable AWU
    //-------- Setup HSE in auto mode --
    CLK_SWCR |= CLK_SWCR_SWEN;              // =bit1, clock SWitch ENable
    CLK_SWR = CLK_SOURCE_HSE;               // =0xB4, enable HSE
    while (CLK_SWCR & CLK_SWCR_SWBSY);
    //----------- Enable CSS -----------------------
    // Clock security system enable & Clock security system detection interrupt enable 
    CLK_CSSR |= (CLK_CSSR_CSSEN | CLK_CSSR_CSSDIE); // =(bit0 + bit3)

Так же нужно будет добавить обработчик второго(CLK) прерывания:

    ; --- CSS HANDLER ------
    xdef _clk_handler
_clk_handler:
    bres CLK_CSSR,#3    ; Clear CSSD
    bres CLK_CSSR,#2    ; Clear CSSDIE
    clr  CLK_CKDIVR     ; fHSI = 16MHz
    iret 

Я предполагаю что мы используем модель сборки по умолчанию т.е. "Debug" в которой оптимизация отключена. Теперь посмотрим как компилятор раскладывает цикл while (CLK_SWCR & CLK_SWCR_SWBSY):

while:
    ld A,_CLK_SWCR
    bcp A,#0x01 ;1 <c_x+0x1>
    jrne while ;8116 <_main+0x1d>

Вышло на три инструкции. Если бы STM8 не имел битовых инструкций, то на этом бы и закончилось. Тип _Bool предполагает, что для обработки таких переменных используются битовые инструкции. В начале функции main() объявим булеву переменную:

_Bool SWBUSY @CLK_SWCR:0;

После чего поменяем цикл:

    while (CLK_SWCR & CLK_SWCR_SWBSY);

на:

    while (SWBUSY);

В этом случае компилятор уже использует битовую инструкцию:

while:
    btjt _CLK_SWCR,#0,exit_from_while ;811b <_main+0x22>
    jrc while ;8116 <_main+0x1d>
exit_from_while:

Если использовать модель сборки Release, то там оба варианта цикла будут реализованы через одну инструкцию btjt, здесь мне придраться не к чему.

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

5 Особенности EEPROM/FLASH подсистемы в микроконтроллерах STM8

В STM8, запись в EEPROM практически не отличается от записи во флеш-память, меняются местами некоторые регистры и флаги, но алгоритм остаётся тем же. Поэтому, если подходить к вопросу алгоритмически, то флеш-память можно рассматривать как дополнительную EEPROM, или наоборот - EEPROM-память рассматривать как довесок к флеш-памяти. Но не следует забывать, что ресурс флеш-памяти в 30 раз ниже чем у EEPROM.

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

  1. Блок - количество байтов которые можно стереть или записать одномоментно.
  2. Страница - некоторое количество блоков. В страницах считается размер областей UBC и PCODE(только для STM8L high-density).
  3. Слово - четыре байта.
  4. MASS - система защиты ППЗУ от нежелательного изменения.
  5. ROP - системы защиты памяти от нежелательного чтения. Устанавливается через Option Bytes.
  6. RWW - позволяет записывать в EEPROM не останавливая при этом работу ядра микроконтроллера. Не доступна на low-density чипах. Запись во флеш-память останавливает работу ядра микроконтроллера на все время записи.
  7. UBC - область пользовательского загрузчика. Размер задаётся в страницах через Option Bytes. Всегда начинается с адреса 0x8000. Таблица векторов соответственно переносится на размер области UBC. UBC закрыта для записи через IAP
  8. PCODE - имеется только в STM8L medium и high density- чипах. Защищённая от записи и чтения область для размещения проприетарного кода. Доступ к этому участку памяти можно получить только через программное (TRAP) прерывание.
  9. IAP - программный программатор, пользовательский загрузчик или любое другое изменение ППЗУ программными средствами.
  10. ICP - аппаратный программатор использующий протокол SWIM.

Область EEPROM в STM8S чипах начинается с адреса 0х4000, в STM8L с 0х1000. Ниже приводится карта памяти для STM8S Medium Density чипов:

Карта памяти для STM8L high-density чипов:

Кстати, в STM32 чипах нет EEPROM памяти вообще. Во флеш там можно писать только страницами. Страница там если не ошибаюсь, равна одному килобайту. И если вам нужно будет записать меньшее количество байт, содержимое страницы придётся куда-то сохранить перед стиранием. Так что, когда вам будут говорить, что STM32F0xx тоже самое что STM8 только лучше, не верьте.

Ок. Я пока предлагаю не трогать такие штуки как UBC и PCODE. Если мы в Option Bytes выставим какое-либо значение UBC отличное от нуля, то лишимся возможности отладки. Значение же PCODE байта вообще выставляется только однажды, сбросить его потом не получится.

Во всех чипах с размером флеш-памяти 16 Кбайт и выше имеется встроенный загрузчик. Он начинается с адреса 0x6000. Блок-схема с алгоритмом его работы представлена ниже:

В отличие от 103-го чипа, в начале трассировки прошивки отладчиком мы попадаем не в таблицу векторов, а именно в этот загрузчик. Его можно увидеть в окне дизассемблера:

Здесь в начале проверяется, есть ли по адресу 0х8000 опкод инструкций INT или JRF. Затем идёт проверка, включён ли bootloader в Option Bytes и нет ли установки ROP. После этого идёт или переход или на адрес 0x8000 или на код загрузчика. В этом фрагменте ещё можно увидеть код переключения на внешний кварц и снятие защиты на запись (MASS) с флеш-памяти и EEPROM. Код загрузчика в STM8L151C8 немного отличается, но суть та же:

6 Регистры подсистемы EEPROM/FLASH

Регистров здесь немного, предлагаю их бегло рассмотреть:

Первый управляющий регистр, сегодня нам не придётся им пользоваться, так что можно принять просто к сведению. Флаги HALT и AHALT позволяют отключать питание с флеш-памяти в режимах энергосбережения halt и active-halt. IE - разрешает прерывание, FIX переключает режим стандартного программирования на быстрое при режиме записи блоками. По умолчанию там стоит ноль, т.е. стандартный режим программирования. Им мы и будем пользоваться.

В L-серии на третьем битe висит EEPM, а на втором WAITM. Они отвечают за перевод флеш-памяти в режим пониженного энергопотребления IDDQ. EEPM делает возможным перевод флеш-памяти в режим пониженного энергопотребления при выполнении программы из ОЗУ, а WAITM переводит флеш-память в режим пониженного энергопотребления IDDQ при ждущем и спящем режимах.

Второй управляющий регистр уже поинтереснее, он переключает режимы записи. OPT - защищает область Option Bytes которая расположена в последнем блоке EEPROM (у S-серии). WPRG - включает 4-х байтный режим записи. ERASE - стирает содержимое блока в "быстром" режиме блочной записи. FPRG - включает "быстрый" режим блочной записи, PRG Включает режим стандартной блочной записи.

Регистру FLASH_CR2 соответствует комплементарный регистр FLASH_NCR2. Т.е. изменяя один регистр, следует изменить и другой, до того как что-то писать в ППЗУ.

Ещё одна комплиментарная пара регистров, которых мы не будем сегодня касаться: FLASH_FPR и FLASH_NFPR. Определяет размер секции защищённой от записи - UBC.

FLASH_PUKR - регистр снятия защиты MASS с флеш-памяти. Для с снятия защиты потребуется последовательно записать в этот регистр числа 0х56 и 0xAE. При записи неверной комбинации, содержимое флеш-памяти будет блокироваться до следующего Reset.

FLASH_DUKR - регистр снятия защиты MASS с EEPROM. Для с снятия защиты потребуется последовательно записать в этот регистр числа 0хAE и 0x56. При записи неверной комбинации, содержимое EEPROM будет заблокировано до следующего Reset.

Статусный регистр. Флаг HVOFF выставляется и сбрасывается аппаратно во время записи в EEPROM. Служит для определения окончания записи в EEPROM для чипов с RWW. В чипах без RWW следить за этим флагом не надо, там выполнение программы "подвешивается" на время записи. DUL - флаг разблокировки EEPROM. PUL Флаг разблокировки флеш-памяти. EOP - конец операции записи. Флаг устанавливается аппаратно, и сбрасывается программно путём чтения регистра или при новых операциях записи/стирании. Служит для определения момента завершения записи во флеш-память. Флаг WG_PG_DIS устанавливается при попытке писать в защищённую область. Используется для определения ошибок в работе.

7 Запись в EEPROM средствами СOSMIC

COSMIC нам существенно упрощает работу когда мы хотим что-то записать в EEPROM или FLASH память.

Для работы с EEPROM/FLASH нам понадобится ещё один заголовочный файл c константами из SPL: stm8s_flash.h Скачать его можно здесь или сделать самому (требуется небольшая доработка). Файл нужно будет поместить в каталог проекта.

Далее добавим в проект файл с исходным кодом следующего содержания:

#include <stdint.h>
#include "iostm8s105.h"
#include "stm8s_flash.h"

@eeprom uint8_t data[16] @0x4010;

void write_to_eeprom(void) {
    uint8_t i;

    if (!(FLASH_IAPSR & 0x02))
    {
        // unlock EEPROM
        FLASH_DUKR = 0xAE;
        FLASH_DUKR = 0x56;
    }
    // check protection off
    while (!(FLASH_IAPSR & FLASH_IAPSR_DUL));
    //write
    for(i=0;i<16;i++)
        data[i]=i;

    FLASH_IAPSR &= ~(FLASH_IAPSR_DUL);      // lock EEPROM
}

Здесь функция write_to_eeprom() записывает некие байты в массив data, определённый в области EEPROM. Используется одно-байтный режим работы.

Остаётся добавить объявление функции void write_to_eeprom(void); в main.c и ПЕРЕД(!) главным циклом поставить вызов этой функции. Замечу, что в процессе отладки бесполезно отслеживать содержимое eeprom. Когда после прошивки мы откроем ST Visual Programmer и считаем содержимое EEPROM то увидим результат работы:

Программа работает должным образом. Но если мы посмотрим на дизассемблерный листинг:

00008120 <_write_to_eeprom>:
     push A
     ld A,_FLASH_IAPSR
     bcp A,#0x08            ; check DUL
     jrne while_loop
     mov _FLASH_DUKR,#0xae
     mov _FLASH_DUKR,#0x56
while_loop:
     ld A,_FLASH_IAPSR
     bcp A,#0x08            ; check DUL
     jreq while_loop
     clr (0x01,SP)
for_loop:
     ld A,(0x01,SP)
     clrw X
     ld XL,A
     ld A,(0x01,SP)
     addw X,#_data
     call c_eewrc
     inc (0x01,SP)
     ld A,(0x01,SP)
     cp A,#0x10
     jrc for_loop
     bres _FLASH_IAPSR,#3   ; reset DUL
     pop A
     ret

То можно увидеть, что для операции присваивания используется вызов непонятной функции (выделено красным). Вызываемая функция выглядит так:

     ld (X),A
wait:
     btjf _FLASH_IAPSR,#2,wait  ; EOP
     ret

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

Теперь попробуем изменить тип данных массива data c uint8_t на uint16_t. Тогда строка кода data[i]=(uint16_t)i; обернётся вызовом такой функции:

    81b5:       9f              ld A,XL
    81b6:       a4 03           and A,#0x03 ;0x3
    81b8:       27 18           jreq 0x81d2 ;0x81d2
    81ba:       4a              dec A
    81bb:       27 22           jreq 0x81df ;0x81df
    81bd:       4a              dec A
    81be:       27 30           jreq 0x81f0 ;0x81f0
    81c0:       7b 03           ld A,(0x03,SP) ;0x3
    81c2:       f7              ld (X),A
    81c3:       7b 04           ld A,(0x04,SP) ;0x4
    81c5:       5c              incw X
    81c6:       72 05 50 5f     btjf 0x505f,#2,0x81c6 ;0x81c6
    81ca:       fb
    81cb:       f7              ld (X),A
    81cc:       72 05 50 5f     btjf 0x505f,#2,0x81cc ;0x81cc
    81d0:       fb
    81d1:       81              ret
    81d2:       90 93           ldw Y,X
    81d4:       1e 03           ldw X,(0x03,SP) ;0x3
    81d6:       bf 00           ldw 0x00,X
    81d8:       93              ldw X,Y
    81d9:       ee 02           ldw X,(0x02,X) ;0x2
    81db:       bf 02           ldw 0x02,X ;0x2
    81dd:       20 1e           jra 0x81fd ;0x81fd
    81df:       5a              decw X
    81e0:       90 93           ldw Y,X
    81e2:       1e 03           ldw X,(0x03,SP) ;0x3
    81e4:       bf 01           ldw 0x01,X ;0x1
    81e6:       93              ldw X,Y
    81e7:       f6              ld A,(X)
    81e8:       b7 00           ld 0x00,A
    81ea:       e6 03           ld A,(0x03,X) ;0x3
    81ec:       b7 03           ld 0x03,A ;0x3
    81ee:       20 0d           jra 0x81fd ;0x81fd
    81f0:       1d 00 02        subw X,#0x0002 ;0x2
    81f3:       90 93           ldw Y,X
    81f5:       1e 03           ldw X,(0x03,SP) ;0x3
    81f7:       bf 02           ldw 0x02,X ;0x2
    81f9:       93              ldw X,Y
    81fa:       fe              ldw X,(X)
    81fb:       bf 00           ldw 0x00,X
    81fd:       93              ldw X,Y
    
    81fe:       72 1c 50 5b     bset 0x505b,#6 ;0x505b   BSET FLASH_CR2,#6 ; set word mode
    8202:       72 1d 50 5c     bres 0x505c,#6 ;0x505c   BRES FLASH_NCR2,#6 ; set word mode
                                ; ==== WRITE 4 BYTES ====
    8206:       b6 00           ld A,0x00
    8208:       f7              ld (X),A
    8209:       b6 01           ld A,0x01 ;0x1
    820b:       e7 01           ld (0x01,X),A ;0x1
    820d:       b6 02           ld A,0x02 ;0x2
    820f:       e7 02           ld (0x02,X),A ;0x2
    8211:       b6 03           ld A,0x03 ;0x3
    8213:       e7 03           ld (0x03,X),A ;0x3
    8215:       72 05 50 5f     btjf 0x505f,#2,0x8215 ;0x8215 ; wait EOP
    8219:       fb

    821a:       81              ret
    821b:       20 fe           jra 0x821b ;0x821b

Здесь я красным обвёл финальную часть функции, где устанавливается 4-х байтный режим записи в регистре FLASH_CR2, после чего идёт сама запись в EEPROM. Результат можно увидеть опять же в STVP:

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

В целом, нужно отдать должное COSMIC, - это замечательный инструмент для работы с EEPROM. Им удобно пользоваться если вам нужно скинуть туда какие-то небольшой объем данных, например: показания сенсора, станции FM-модуля, но если мы хотим писать во флеш-память и EEPROM блоками, то нам нужно научиться обходиться без его помощи.

8 Безопасное снятие защиты MASS, одно-байтный режим записи в EEPROM/FLASH

Но прежде всего мне хотелось бы коснуться, вопроса о защите от дребезга питания или от неопределённого поведения при разблокировке MASS. На мой взгляд, последовательная запись ключей в регистр разблокировки не очень хорошая идея. Будет лучше, если объявить две глобальные переменные которые будут хранить в оперативной памяти значения ключей. Глобальные переменные инициализируются в начале работы программы, и перед их использованием должно пройти какое-то время. Это даёт некоторую защиту от неопределённого поведения. Идею можно развить и сделать как в защите серийных номеров от дизассамблирования, когда номера хранятся не в чистом виде, а являются результатом вычисления какой-нибудь функции. Допустим, можно объявить глобальный массив, со значениями которого будут производиться некие арифметические операции, в результате выполнения которых будут выдаваться числа: 0хАЕ и 0х56. В идеале можно написать генератор псевдослучайного числа, который при инициализации тем или иным значением будет выдавать нужные числа.

Писать генератор псевдослучайного числа мне показалось чересчур и поэтому в онлайн-редакторе я на скорую руку состряпал пару функций:

uint8_t keys[]={0,1,2,3,4,5,6,7,8,9};

uint8_t get_0xAE_number(void) {
    unsigned int num=3;
    char i, j;
    for(j=0;j<20;j++)
        for(i=0;i<7;i++)
            num+=keys[i]-(i+1);
    num &=0x0005f;  num <<=1;
    return (uint8_t)num;
}

uint8_t get_0x56_number(void) {
    char num=5;
    char i, j;
    for(j=0;j<18;j++)
        for(i=0;i<5;i++)
            num+=keys[i]-(i+1);
    num &=0x7F; num <<=1;
    return (uint8_t)num;
}

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

00008108 <_get_0xAE_number>:
        sub SP,#0x06
        ldw X,#0x0003
        ldw (0x04,SP),X
        clr (0x03,SP)
for_j:
        clr (0x06,SP)
for_i:
        ld A,#_keys
        ld XL,A
        ld A,#0x01
        add A,(0x06,SP)
        jrnc 0x811d
        incw X
        rlwa X,A
        ldw (0x01,SP),X
        rrwa X,A
        ld A,(0x06,SP)
        clrw X
        ld XL,A
        ld A,(_keys,X)
        clrw X
        ld XL,A
        subw X,(0x01,SP)
        addw X,(0x04,SP)
        ldw (0x04,SP),X
        inc (0x06,SP)
        ld A,(0x06,SP)
        cp A,#0x07
        jrc for_i
        inc (0x03,SP)
        ld A,(0x03,SP)
        cp A,#0x14
        jrc for_j
        ld A,(0x05,SP)
        and A,#0x5f
        ld (0x05,SP),A
        clr (0x04,SP)
        sll (0x05,SP)
        rlc (0x04,SP)
        ld A,(0x05,SP)
        addw SP,#0x06
        ret

00008152 <_get_0x56_number>:
        sub SP,#0x03
        ld A,#0x05
        ld (0x02,SP),A
        clr (0x01,SP)
for_j:
        clr (0x03,SP)
for_i:
        ld A,(0x03,SP)
        clrw X
        ld XL,A
        ld A,(0x03,SP)
        inc A
        sub A,(_keys,X)
        neg A
        add A,(0x02,SP)
        ld (0x02,SP),A
        inc (0x03,SP)
        ld A,(0x03,SP)
        cp A,#0x05
        jrc for_i
        inc (0x01,SP)
        ld A,(0x01,SP)
        cp A,#0x12
        jrc for_j
        ld A,(0x02,SP)
        and A,#0x7f
        ld (0x02,SP),A
        sll (0x02,SP)
        ld A,(0x02,SP)
        addw SP,#0x03
        ret

Если весь этот код выполняется успешно, то ППЗУ разблокируется. Если нет, то происходит блокировка до следующего Reset.

Самописная функция write_to_eeprom() у меня получилась такой:

void write_to_eeprom(void) {
    uint8_t i;
    uint8_t * adr=data;
    // if eeprom locked
    if (!(FLASH_IAPSR & FLASH_IAPSR_DUL))
    {
        // then unlock EEPROM
        FLASH_DUKR = get_0xAE_number();
        FLASH_DUKR = get_0x56_number();
    }
    // check protection off
    while (!(FLASH_IAPSR & FLASH_IAPSR_DUL));
    //write
    for (i=0;i<16;i++, adr++) {
        *adr=i;
        while (!(FLASH_IAPSR & (FLASH_IAPSR_HVOFF | FLASH_IAPSR_WR_PG_DIS)));
    }

    FLASH_IAPSR &= ~(FLASH_IAPSR_DUL);      // lock EEPROM

}

где data - это следующий массив:

@eeprom uint8_t data[16] @0x4010;

В отличии от варианта COSMIC'а, здесь ожидается установка флага HVOFF вместо EOP (так делается в SPL) в качестве признака окончания записи. Кроме того, здесь анализируется флаг FLASH_IAPSR_WR_PG_DIS на случай, если что-то пойдёт не так. В low-density чипах без RWW ждать окончания записи не нужно. Там на время записи приостанавливается работа CPU. Учтите, что прерывания тоже перестают работать, так что там можно писать байты без всяких задержек. Я сам не побывал работать с EEPROM на STM8S103, но здесь: "STM8S EEPROM надо ли ждать EOP флага после записи 1 байта? - Форум разработчиков электроники ELECTRONIX.ru" утверждается, что все именно так и обстоит.

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

00008187 <_write_to_eeprom>:
     sub SP,#0x03
     ldw X,#_data
     ldw (0x01,SP),X
     ld A,_FLASH_IAPSR
     bcp A,#0x08

     jrne 0x81a0
     call _get_0xAE_number
     ld _FLASH_DUKR,A
     callr _get_0x56_number
     ld _FLASH_DUKR,A
while_1:
     ld A,_FLASH_IAPSR
     bcp A,#0x08

     jreq while_1
     clr (0x03,SP)
for:
     ld A,(0x03,SP)
     ldw X,(0x01,SP)
     ld (X),A
while_2:
     ld A,_FLASH_IAPSR
     bcp A,#0x41

     jreq while_2
     inc (0x03,SP)
     ldw X,(0x01,SP)
     addw X,#0x0001
     ldw (0x01,SP),X
     ld A,(0x03,SP)
     cp A,#__endzp
     jrc for
     bres _FLASH_IAPSR,#3   ; reset DUL
     addw SP,#0x03
     ret

Функция записи во флеш-память будет не сильно отличаться:

void write_to_flash(void) {
    uint8_t i;
    uint8_t * adr=(uint8_t*)0x8500;
    // if eeprom locked
    if (!(FLASH_IAPSR & FLASH_IAPSR_PUL))
    {
        // then unlock EEPROM
        FLASH_PUKR = get_0x56_number();
        FLASH_PUKR = get_0xAE_number();
    }
    // check protection off
    while (!(FLASH_IAPSR & FLASH_IAPSR_PUL));
    //write
    for (i=0;i<16;i++, adr++) {
        *adr=i;
        while (!(FLASH_IAPSR & (FLASH_IAPSR_EOP | FLASH_IAPSR_WR_PG_DIS)));
    }

    FLASH_IAPSR &= ~(FLASH_IAPSR_PUL);      // lock EEPROM
}

9 Четырехбайтный режим записи в EEPROM/FLASH

Четырех-байтный режим записи устанавливается флагами WPRG в регистре FLASH_CR2 и NWPRG в FLASH_NCR2. От однобайтного режима отличается увеличением скорости записи в четыре раза, т.к. за одну операцию записи пишется разом четыре байта.

Алгоритм практически аналогичен предыдущим примерам:

void write_to_eeprom(void) {
    uint16_t i;
    uint8_t * adr=data;
    // if eeprom locked
    if (!(FLASH_IAPSR & FLASH_IAPSR_DUL))
    {
        // then unlock EEPROM
        FLASH_DUKR = get_0xAE_number();
        FLASH_DUKR = get_0x56_number();
    }
    // check protection off
    while (!(FLASH_IAPSR & FLASH_IAPSR_DUL));
    // set 4-bytes mode
    FLASH_CR2 |= FLASH_CR2_WPRG;
    FLASH_NCR2 &= ~(FLASH_NCR2_NWPRG);
    //write
    i=0; while (i<16) {
        *(adr++)=(uint8_t)i;
        *(adr++)=(uint8_t)(i>>8);
        i++;
        *(adr++)=(uint8_t)i;
        *(adr++)=(uint8_t)(i>>8);
        i++;
        while (!(FLASH_IAPSR & (FLASH_IAPSR_HVOFF | FLASH_IAPSR_WR_PG_DIS)));
    }

    FLASH_IAPSR &= ~(FLASH_IAPSR_DUL);      // lock EEPROM

}

void write_to_flash(void) {
    uint16_t i;
    uint8_t * adr=(uint8_t*)0x8500;
    // if eeprom locked
    if (!(FLASH_IAPSR & FLASH_IAPSR_PUL))
    {
        // then unlock EEPROM
        FLASH_PUKR = get_0x56_number();
        FLASH_PUKR = get_0xAE_number();
    }
    // check protection off
    while (!(FLASH_IAPSR & FLASH_IAPSR_PUL));
    // set 4-bytes mode
    FLASH_CR2 |= FLASH_CR2_WPRG;
    FLASH_NCR2 &= ~(FLASH_NCR2_NWPRG);
    //write
    i=0; while (i<16) {
        *(adr++)=(uint8_t)i;
        *(adr++)=(uint8_t)(i>>8);
        i++;
        *(adr++)=(uint8_t)i;
        *(adr++)=(uint8_t)(i>>8);
        i++;
        while (!(FLASH_IAPSR & (FLASH_IAPSR_EOP | FLASH_IAPSR_WR_PG_DIS)));
    }

    FLASH_IAPSR &= ~(FLASH_IAPSR_PUL);      // lock EEPROM
}

где data - это следующий массив:

@eeprom uint8_t data[16] @0x4010;

Обращаю внимание, что здесь запись двухбайтных данных организована в классическом "перевёрнутом" порядке. Т.е. сначала пишется младший байт, затем старший.

10 Блоковый режим записи в EEPROM/FLASH. Копирование кода в ОЗУ и выполнение его оттуда средствами COSMIC

Последний самый интересный режим записи - блоковый режим. Позволяет за раз записать в ППЗУ целый блок - 128 байт. Имеется два варианта этого режима: стандартный и быстрый. Я буду рассматривать стандартный режим как самый удобный, требующий меньше телодвижений и следовательно, с ним меньше шансов наделать ошибок. Быстрый режим очень походит на режим записи флеш-памяти в STM32.

Самое сложное в этом режиме то, запись должна выполняться из ОЗУ. Данные должны быть выровнены по границам блока. В STM32 есть регистр в котором можно выбирать записываемую страницу, в STM8 местоположение блока придётся рассчитывать самим.

Я ничего не нашёл в документации относительно особенностей чипов 208/207/007, но анализ кода SPL для этих чипов говорит о том, что там за одну операцию можно записать целую страницу, это четыре блока по 128Б т.е. 512 байт. Сам лично пока не проверял.

Копирование кода в ОЗУ подробно рассмотрено в руководстве: Application note - AN2659, - глава пятая - "Configuring the Cosmic compiler for RAM code execution".

Весь процесс примерно делится на три этапа.

Этап первый. Перед началом функций write_to_eeprom() и write_to_flash() нужно поставить объявление новой секции: #pragma section(FLASH_CODE)

После функций write_to_eeprom() и write_to_flash() нужно будет объявить завершение новой секции: #pragma section()

Этап второй. В настройках проекта перейти на вкладку компоновщика, выбрать там категорию Input, и щёлкнув правой кнопкой мыши по сегменту "RAM" добавить туда новую секцию ".FLASH_CODE" (точку в префиксе не забудьте):

Щёлкнув правой кнопкой по ряду "Option" добавленной секции, измените пустое поле на "-ic". Это даст линковщику команду считать данную секцию перемещаемым кодом. При ее линковке не будет использоваться абсолютная адресация (проверьте!).

Этап третий. Теперь нам понадобится механизм копирования секции в OЗУ. В COSMIC для этого существует готовая функция: int _fctcpy(char name). она поставляется в ассемблерном виде, в файле fctcpy.s. Его нужно будет скопировать в папку проекта и далее добавить к проекту.

Осталось дело за малым. Нужно добавить объявление функции в начало файла main.c:

extern int _fctcpy(char name);

В качестве параметра функции используется первая буква в названии секции. В начало функции main() вставим вызов этой функции: _fctcpy('F'); и дело в шляпе. Это всё.

Пример функций write_to_eeprom() и write_to_flash() реализующих запись двух блоков в EEPROM и флеш-память приведён ниже:

void write_to_eeprom(void) {
    uint8_t i,page;
    uint8_t * adr=data;
    // if eeprom locked
    if (!(FLASH_IAPSR & FLASH_IAPSR_DUL))
    {
        // then unlock EEPROM
        FLASH_DUKR = get_0xAE_number();
        FLASH_DUKR = get_0x56_number();
    }
    // check protection off
    while (!(FLASH_IAPSR & FLASH_IAPSR_DUL));
    // set standart block mode
    FLASH_CR2 |= FLASH_CR2_PRG;
    FLASH_NCR2 &= ~(FLASH_NCR2_NPRG);
    //write
    for(page=0;page<2;page++) {
        for (i=0;i<128;i++,adr++) {
            *adr=i;
        }
        while (!(FLASH_IAPSR & (FLASH_IAPSR_HVOFF | FLASH_IAPSR_WR_PG_DIS)));
    }

    FLASH_IAPSR &= ~(FLASH_IAPSR_DUL);      // lock EEPROM

}

void write_to_flash(void) {
    uint8_t i, page;
    uint8_t * adr=(uint8_t*)0x8500;
    // if eeprom locked
    if (!(FLASH_IAPSR & FLASH_IAPSR_PUL))
    {
        // then unlock EEPROM
        FLASH_PUKR = get_0x56_number();
        FLASH_PUKR = get_0xAE_number();
    }
    // check protection off
    while (!(FLASH_IAPSR & FLASH_IAPSR_PUL));
    // set standart block mode
    FLASH_CR2 |= FLASH_CR2_PRG;
    FLASH_NCR2 &= ~(FLASH_NCR2_NPRG);
    //write
    for(page=0;page<2;page++) {
        for (i=0;i<128;i++,adr++) {
            *adr=i;
        }
        while (!(FLASH_IAPSR & (FLASH_IAPSR_EOP | FLASH_IAPSR_WR_PG_DIS)));
    }

    FLASH_IAPSR &= ~(FLASH_IAPSR_PUL);      // lock EEPROM
}

Где массив data в этот раз начинается с начала блока:

@eeprom uint8_t data[16] @0x4000;

В основе лежит алгоритм однобайтной записи, просто добавлен ещё один цикл для записи блока.

Проверяем. Ставим точку останова на вызове функции write_to_eeprom() и смотрим куда ведёт вызов функции:

Вызов идёт в ОЗУ, как и должно быть. Прыгаем, и в окне дизассемлера видим код функций write_to_eeprom() и write_to_flash() перенесённых сюда с помощью _fctcpy().

Результат работы как обычно можно посмотреть в STVP. На этом всё. Остался не рассмотренным быстрый режим блочной записи. Если когда-нибудь соберусь писать статью по написанию своего загрузчика, то начну именно с этого режима.

В заключении могу ещё рекомендовать почитать Executing code from RAM on STM8 | lujji, где автор исследовал возможности перемещения исполняемого кода в ОЗУ для компилятора SDCC. Мне лично система показалась чересчур "костыльной", но возможно вы будете другого мнения.

Скачать исходники, workspace с проектами и прошивками к статье можно по следующей ссылке: article_eeprom.zip