Когда я писал драйвер 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.
Содержание статьи:
Скачать исходники, workspace с проектами и скомпилированными прошивками к статье можно можно будет по ссылке в конце статьи.
В 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 byand 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.
Теперь заменим функцию задержки на ассемблерный вариант. Для этого добавим в проект файл с именем, допустим: "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 для полного дизассемблирования, но тогда на выходе получим "портянку" без указания периферийных регистров и сегментов.
Теперь займёмся прерываниями. Предполагаем, что обработчики прерываний мы будем писать на ассемблере. Тогда первое за что следует взяться, это файл по умолчанию входящий в шаблон проекта: "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 разрешает прерывания.
В 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, то в свойствах проекта нужно будет задать версию своего чипа:
В STM8, запись в EEPROM практически не отличается от записи во флеш-память, меняются местами некоторые регистры и флаги, но алгоритм остаётся тем же. Поэтому, если подходить к вопросу алгоритмически, то флеш-память можно рассматривать как дополнительную EEPROM, или наоборот - EEPROM-память рассматривать как довесок к флеш-памяти. Но не следует забывать, что ресурс флеш-памяти в 30 раз ниже чем у EEPROM.
Одномоментно записать можно: байт, слово или блок. Давайте разбираться, что есть что.
Область 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 немного отличается, но суть та же:
Регистров здесь немного, предлагаю их бегло рассмотреть:
Первый управляющий регистр, сегодня нам не придётся им пользоваться, так что можно принять просто к сведению. Флаги 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 устанавливается при попытке писать в защищённую область. Используется для определения ошибок в работе.
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 блоками, то нам нужно научиться обходиться без его помощи.
Но прежде всего мне хотелось бы коснуться, вопроса о защите от дребезга питания или от неопределённого поведения при разблокировке 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 }
Четырех-байтный режим записи устанавливается флагами 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;
Обращаю внимание, что здесь запись двухбайтных данных организована в классическом "перевёрнутом" порядке. Т.е. сначала пишется младший байт, затем старший.
Последний самый интересный режим записи - блоковый режим. Позволяет за раз записать в ППЗУ целый блок - 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