Преамбула
Изначально вторая глава задумывалась только, как шпаргалка по работе из оперативной памяти, но делать и разбираться в этом не очень трудно. Основная "запара" может настигнуть несведущего именно при работе с прерываниями. Собсна, решено объединить.
Стек:
Отладочная плата LDM-HELPER-K1986BE1QI-FULL;
МК K1986BE1QI(но все паки и тд. юзаю 1986ВЕ1Т ибо
те же яйца, только с бокудаже даташит 1 на целую линейку);Программатор ARM-USB-OCD-H;
IDE Keil uVison 5;
Open OCD.
Оффтоп
"RAM & Interruptions" не потому что "Я умею писать по буржуйски", а потому что "Размер заголовка не может превышать 120 символов" и "Отладка прерываний в опреативной памяти" не лезет. Ну, как говорил препод по информатике: "Это по молодости кажется, что не влазит"
Подготовка к работе
В принципе, подготовка ничем не отличается от предыдущего поста, поэтому повторяться лишний раз не будем.
Настройка Keil для линковки проекта в оперативную память
Для того, чтобы собрать проект под 1986BE1T (и не только) для отладки с помощью OLIMEX ARM-USB-OCD-H (и не только) из оперативной памяти средствами среды Keil uVision 5 необходимо для начала создать новый проект keil (смотри все тот же предыдущий пост).

После того, как проект будет создан, необходимо как-то объяснить линкеру, что именно мы хотим. Посмотрим, где и как это делать. Если жамкнуть правой кнопкой по "target" в дереве проекта можно увидеть опции – заходим туда.
С «Output» все просто – минимально необходимое условие выполнения наших целей – это установленный флаг «Create Executable».
«Target» и «Linker» же немного более интересны. На выходе конфигурации этих вкладок получим готовые скрипты для линкера с помощью так называемых «Scatter files». Что это за скатерть такая объяснять не будем, есть документация.

На вкладке «Linker» нужно включить флаг «Use Memory Layout from Target Dialog» - это позволит автогенерить скрипты в зависимости от того, как мы настроим вкладку «Target».
Собственно, к "Target".

Что видим тут полезного для поставленной цели, так это 2 блока с описанием памяти (причем предзаполненных! Спасибо пакам keil-а.).
Посмотрим, какая скатерть получается на выходе. В папке проекта есть ./RTE/Device/MDR1986BE1T, а там файл MDR1986VE1T.sct. Это дефолтный скрипт. Так как никаких изменений не проводилось будет юзаться именно он.
MDR1986VE1T.sct
; ****************************************************************************** ; ************* Scatter-Loading Description File for MDR1986VE1T *************** ; ****************************************************************************** LR_IROM1 0x00000000 0x00020000 { ; load region size_region ER_IROM1 0x00000000 0x00020000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) } RW_IRAM1 0x20000000 0x00008000 { ; RW data .ANY (+RW +ZI) } RW_IRAM2 0x20100000 0x00004000 { .ANY (+RW +ZI) } }
Небольшой анализ показывает, что код будет записываться в ER_IROM1. Правда сейчас там все сконфигурировано так, чтобы грузить программу во флешу. Будем менять.
Можно, кончено, править scatter руками, но проще воспользоваться рафическими инструментами. Инструкции линкуются в первый раздел IROM памяти. Т.е. изменив поля «Start» и «Size» IROM1 на вкладке «Target» можно получить нужную линковку. Видим, что разделов IRAM у нас 2. Но. Читая даташит натыкаемся на фразу: «AHB-Lite – шина выборки инструкций и данных из внешнего адресного пространства» и, немного покурив остальное описание понимаем, что эта шина – единственный способ дотянуться до инструкций в оперативной памяти. Смотрим адреса и понимаем, что программу для выполнения в оперативной памяти дозволено грузить с 0x20100000.

Итого: копируем поля из IRAM2 в IROM1 и ребилдим.
Результирующий scatter файл. Будет находиться ./Objects/projectName.sct
; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x20100000 0x00004000 { ; load region size_region ER_IROM1 0x20100000 0x00004000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) } RW_IRAM1 0x20000000 0x00008000 { ; RW data .ANY (+RW +ZI) } }
Теперь видно, что инструкции будут располагаться в оперативной памяти, причем именно в той, которая для них и предназначена. Отлично! Пора проверить.
Запуск в оперативной памяти
Загрузим проект по образу и подобию 1 части статьи. И посмотрим адреса функций, например:
(gdb) x main 0x20100aa8 <main>: 0x25d95598
Т.е. адрес входа в main - 0x20100aa8, что вполне себе похоже на то, ради чего все это было затеяно.
Попробуем простенькую программу. Пусть по прерыванию таймера зажжем диод на плате.
Вначале было слово тактирование. С этого и начнем. Еще с первых дней знакомства с Миландровскими вычислителями был написан небольшой блочок, для того, чтобы в будущих простых проектах не париться по поводу настройки частоты проца. Суть: работает от внутреннего HSI, разрешает задавать частоты от 8 до 128 МГц. Пока что этого хватало.
Рассмотрим программу, которую будем грузить в RAM. Код туп, прост и, надеюсь, понятен.
main.c
static void ledInit( void ); inline static void ledOn( void ) { MDR_PORTB->RXTX = 0; } inline static void ledOff( void ) { MDR_PORTB->RXTX = 1; } static void timerInit( void ); inline static void timerStart( void ) { MDR_TIMER1->CNTRL = 0x01; } void TIMER1_IRQHandler( void ); int main( void ) { // Основное тактирование freqHsiCpuSet( 8 ); // Инициализация диода ledInit(); // Инициализация таймера timerInit(); // Стартуем! Я сказала стар-ту-ем. timerStart(); while(1) __NOP(); } static void ledInit( void ) { MDR_RST_CLK->PER_CLOCK |= 1 << 22; PORT_InitTypeDef portLEDInitStruct; PORT_StructInit( &portLEDInitStruct ); portLEDInitStruct.PORT_OE = PORT_OE_OUT; portLEDInitStruct.PORT_MODE = PORT_MODE_DIGITAL; portLEDInitStruct.PORT_SPEED = PORT_SPEED_SLOW; portLEDInitStruct.PORT_Pin = PORT_Pin_0; PORT_Init( MDR_PORTB, &portLEDInitStruct ); ledOff(); } static void timerInit( void ) { NVIC_EnableIRQ( TIMER1_IRQn ); // Разрешение тактирования TIMER1 MDR_RST_CLK->PER_CLOCK |= 1 << 14; // Разрешение тактовой частоты на TIM1 MDR_RST_CLK->TIM_CLOCK |= 1 < 24; MDR_TIMER1->CNTRL = 0; // Настраиваем работу основного счетчика // Начальное значение MDR_TIMER1->CNT = 0; //Предделитель частоты MDR_TIMER1->PSG = 0; // Основание счета MDR_TIMER1->ARR = 0x0F; // Разрешение генерировать прерывание при CNT = ARR MDR_TIMER1->IE = 1 << 1; } void TIMER1_IRQHandler( void ) { ledOn(); MDR_TIMER1->IE = 0; NVIC_DisableIRQ( TIMER1_IRQn ); }
Что ожидается? А того, что после запуска практически сразу будет сработано прерывание от таймера. Окей. Грузим в оперативку. Запускаем.
Аккуратный вывод «Continuing.» говорит о том, что программа запущена, но диод не горит(
В чем же, собственно, дело?
Приостановим программу и увидим,
(gdb) <Cntrl + c> Program received signal SIGINT, Interrupt. main () at main.c:29 29 while(1) __NOP();
... что мы в бесконечном цикле ожидания.
Первая мысль (конечно же, после того, как выругал компилятор/отладчик/среду и тд. и тп. ибо виноват ТОЧНО не ты) - возможно не так проинициализировл LED. Ну, энивей, прерывание сработать должно, попробуем в него попасть, поставив точку останова в обработчике прерывания таймера:
(gdb) b TIMER1_IRQHandler Breakpoint 2 at 0x20100192: TIMER1_IRQHandler. (2 locations)
Перезапустим:
(gdb) monitor reset halt (gdb) load (gdb) c
Ничего не изменилось(
Глянем состояние регистров таймера. Счетчик (Регистр CNT) лежит по адресу 0x40070000, а Основание (Регистр ARR) по 0x40070000.
(gdb) x 0x40070000 0x40070000: 0x0000 (gdb) x 0x40070000 0x40070000: 0x0000
Таким же образом можно проверить флаги разрешения прерываний и прочее. И, судя по, что счетчик досчитал до основания и прерывания разрешены (trust me i am an engineer) все должно было сработать. Но не сработало.
А теперь попробуем провернуть все тоже самое, но во флеше. Проводим нехитрые манипуляции с вкладкой "Target", ребилдим, запускаем.
Загрузились, запустились – все работает. И диод горит в прерывании остановились.
(gdb) c Continuing. Breakpoint 1, main () at main.c:18 18 freqHsiCpuSet( 8 ); (gdb) c Continuing. Breakpoint 2, TIMER1_IRQHandler () at main.c:77 77 ledOn(); (gdb) c Continuing.
Суть в том, что таблица векторов прерываний в данных МК непереносима в принципе и располагается по нулевому адресу. Убедимся, рассмотрев файл startup_MDR1986VE1T.S. Там происходит начальная инициализация: стек поинтер, размеры кучи, стека… Но сейчас будет интересовать таблица векторов прерываний.
startup_MDR1986VE1T (кусочек)
; Vector Table Mapped to Address 0 at Reset AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD __initial_sp ; Top of Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler DCD HardFault_Handler ; Hard Fault Handler DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD SVC_Handler ; SVCall Handler DCD 0 ; Reserved DCD 0 ; Reserved DCD PendSV_Handler ; PendSV Handler DCD SysTick_Handler ; SysTick Handler ; External Interrupts DCD MIL_STD_1553B2_IRQHandler ;IRQ0 DCD MIL_STD_1553B1_IRQHandler ;IRQ1 DCD USB_IRQHandler ;IRQ2 DCD CAN1_IRQHandler ;IRQ3 DCD CAN2_IRQHandler ;IRQ4 DCD DMA_IRQHandler ;IRQ5 DCD UART1_IRQHandler ;IRQ6 DCD UART2_IRQHandler ;IRQ7 DCD SSP1_IRQHandler ;IRQ8 DCD BUSY_IRQHandler ;IRQ9 DCD ARINC429R_IRQHandler ;IRQ10 DCD POWER_IRQHandler ;IRQ11 DCD WWDG_IRQHandler ;IRQ12 DCD TIMER4_IRQHandler ;IRQ13 DCD TIMER1_IRQHandler ;IRQ14 DCD TIMER2_IRQHandler ;IRQ15 DCD TIMER3_IRQHandler ;IRQ16 DCD ADC_IRQHandler ;IRQ17 DCD ETHERNET_IRQHandler ;IRQ18 DCD SSP3_IRQHandler ;IRQ19 DCD SSP2_IRQHandler ;IRQ20 DCD ARINC429T1_IRQHandler ;IRQ21 DCD ARINC429T2_IRQHandler ;IRQ22 DCD ARINC429T3_IRQHandler ;IRQ23 DCD ARINC429T4_IRQHandler ;IRQ24 DCD 0 ;IRQ25 DCD 0 ;IRQ26 DCD BKP_IRQHandler ;IRQ27 DCD EXT_INT1_IRQHandler ;IRQ28 DCD EXT_INT2_IRQHandler ;IRQ29 DCD EXT_INT3_IRQHandler ;IRQ30 DCD EXT_INT4_IRQHandler ;IRQ31
Эта же таблица (только External Interrupts) описана в разделе 31.10 «Lock-up» даташита.
А вот в пункте 31.1 «Типы исключений» написано: "В таблице представлены типы исключений, их номера и приоритет. Номер показывает словное смещение исключений относительно стартового адреса таблицы векторов, которая всегда расположена в 0x0."
После небольшого когнитивного напряжения замечаем, что таблица «железно» вбивается в нулевой адрес. Заметить это нужно очень и очень, ибо с этим пришлось повозиться. Сразу было неочевидно. (Ну, мб я дурень, а остальные до этого сразу догадываются).
Ок, посмотрим на таблицу изнутри.
(gdb) x/48 0x0 0x0 <RST_CLK_GetClocksFreq>: 0x20002068 0x00000165 0x0000016d 0x0000016f 0x10 <RST_CLK_GetClocksFreq+16>: 0x00000000 0x00000000 0x00000000 0x00000000 0x20 <RST_CLK_GetClocksFreq+32>: 0x00000000 0x00000000 0x00000000 0x00000171 0x30 <RST_CLK_GetClocksFreq+48>: 0x00000000 0x00000000 0x00000173 0x00000175 0x40 <RST_CLK_GetClocksFreq+64>: 0x00000177 0x00000179 0x0000017b 0x0000017d 0x50 <RST_CLK_GetClocksFreq+80>: 0x0000017f 0x00000181 0x00000183 0x00000185 0x60 <RST_CLK_GetClocksFreq+96>: 0x00000187 0x00000189 0x0000018b 0x0000018d 0x70 <RST_CLK_GetClocksFreq+112>: 0x0000018f 0x00000191 0x0000083d 0x00000195 0x80 <RST_CLK_GetClocksFreq+128>: 0x00000197 0x00000199 0x0000019b 0x0000019d 0x90 <RST_CLK_GetClocksFreq+144>: 0x0000019f 0x000001a1 0x000001a3 0x000001a5 0xa0 <RST_CLK_GetClocksFreq+160>: 0x000001a7 0x00000000 0x00000000 0x000001a9 0xb0 <RST_CLK_GetClocksFreq+176>: 0x000001ab 0x000001ad 0x000001af 0x000001b1
Так как таблица - это вполне себе символ можно указать
(gdb) x/48 __Vectors
Выведет тоже самое.
Почему 48? 32 внешних прерывания + адрес вершины стека + 6 системных прерываний + 9 зарезервированных полей.
Заметим, что наше прерывание от таймера (TIMER1_IRQHandler) – это IRQ14. Т.е. (16 + 14) * 4 = 120(в дес) ил 0x78. Что лежит по этому адресу?
(gdb) x/ 0x78 0x78 <RST_CLK_GetClocksFreq+120>: 0x0000083d
А куда указывает?
(gdb) x 0x0000083d 0x83d <TIMER1_IRQHandler>: 0x8ff000b5
Есть! То самое прерывание, которое искали.
Подобное можно выщучить из таблицы. Получается строка 0x70 <RST_CLK_GetClocksFreq+112> столбец 3, т.к. 1 слово = 4 байта. Кстати, выглядит это все примерно так:

Вернемся немного назад и провернем все тоже самое, но в оперативке. Глянем таблицу.
(gdb) x/48 0
Осталась неизменной, что, наверное и следовало ожидать, ведь флеш не прошивалась.
А что лежит по
(gdb) x 0x0000083d 0x83d: 0x8ff000b5
Собственно, то, что и было, но без символа.
Заметил только при написании статьи
Возможно теперь это отработает правильно (ибо флеш прошита такой же программой, что и в оперативке, но мы "зайдем" не в тот обработчик, что написали, а в тот, что прошит). Для чистоты эксперимента лучше очистить флешку.
А глянем теперь таблицу через символ
(gdb) x/48 __Vectors 0x20100000 <__Vectors>: 0x20002068 0x20100165 0x2010016d 0x2010016f 0x20100010: 0x00000000 0x00000000 0x00000000 0x00000000 0x20100020: 0x00000000 0x00000000 0x00000000 0x20100171 0x20100030: 0x00000000 0x00000000 0x20100173 0x20100175 0x20100040: 0x20100177 0x20100179 0x2010017b 0x2010017d 0x20100050: 0x2010017f 0x20100181 0x20100183 0x20100185 0x20100060: 0x20100187 0x20100189 0x2010018b 0x2010018d 0x20100070: 0x2010018f 0x20100191 0x2010083d 0x20100195 0x20100080: 0x20100197 0x20100199 0x2010019b 0x2010019d 0x20100090: 0x2010019f 0x201001a1 0x201001a3 0x201001a5 0x201000a0: 0x201001a7 0x00000000 0x00000000 0x201001a9 0x201000b0: 0x201001ab 0x201001ad 0x201001af 0x201001b1
Есть родная, причем наше прерывание туц описано правильно. Осталось переместить ее во флешу. К сожалению чистых способов я не знаю, затрагивать флешку придется энивей.
Последний рывочек
Есть несколько способов добиться желаемого результата, но вот этот мне нравится больше всех: прошить во флешку обработчик прерывания, который будет переходить по нужному адресу в оперативной памяти.
Создаем новый проект, который будет располагаться во флеш
#define READWD(x) (*((volatile uint32_t *)(x))) typedef void ( *fUserIrptHandler )( void ); int main() { while(1){} } void TIMER1_IRQHandler() { volatile uint32_t addr = READWD( 0x20100078 ); fUserIrptHandler funcInRAM = ( fUserIrptHandler )( addr ); funcInRAM(); }
Все предельно просто. При срабатывании прерывания, по таблице попадем в тот обработчик, который лежит во флеш. В нем же мы переходим к пользовательскому обработчику, расположенному уже в оперативной памяти. Такая получается матрешка.
Смещение постоянное, так что для расчета не тяжко: Стартовый адрес памяти (0x20100000) + сдвиг для конкретного прерывания (0x78).
Прошиваем данную флеш вспомогательной программой. Запускаем ту программу, что с таймером из оперативной. Baldezh.
Итого
Да, совсем обойтись без флехи не удалось, но всегда нужно искать плюсы:
Если такой способ есть гуру обязательно подскажет о нем в комментарии - буду рад;
Погрузились во внутрянку работы проца, это полезно;
Все-таки прошить можно 1 раз флешку нужными обработчиками и уйму раз отлаживаться в оперативке, не коцая постоянку. Есть конечно, трабл: если во флешке будет прошит обработчик, а в оперативке мы его не сделаем - будет доступ не туда, куда надо - а это беда.
Спасибо, надеюсь кому-то статья поможет :)
