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