Преамбула

Изначально вторая главазадумывалась только, как шпаргалка по работе из оперативной памяти, но делать и разбираться в этом не очень трудно. Основная "запара" может настигнуть несведущего именно при работе с прерываниями. Собсна, решено объединить.

Стек:

  • Отладочная плата 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 раз флешку нужными обработчиками и уйму раз отлаживаться в оперативке, не коцая постоянку. Есть конечно, трабл: если во флешке будет прошит обработчик, а в оперативке мы его не сделаем - будет доступ не туда, куда надо - а это беда.

Спасибо, надеюсь кому-то статья поможет :)

Комментарии (0)