Мое описание не будет изобиловать сложными техническими подробностями - за ними лучше всего обратиться к полезным ссылкам в конце статьи. Пост подойдет для людей, которые хотят просто взять и написать свою первую программу для ARM7 и, в дальнейшем, радоваться этому событию.
Наша простейшая программа не будет работать под управлением какой-либо встраиваемой операционной системы - все будет "крутиться" на голом железе. А раз так, то нам придется позаботиться об инициализации различных аппаратных штуковин и программных стеков - LPC2292 это вам не ADuC812!
Инициализация микроконтроллера и последующий переход к выполнению функции main() осуществляется при помощи так называемого startup-кода. Можно написать его на C, но мы будем делать это на ассемблере - так проще и нагляднее.
Исполнение программы начинается с функции _start. Для начала, надо заполнить таблицу векторов прерываний переходами к соответствующим обработчикам - наша программа будет залита загрузчиком в Flash-память начиная с адреса 0x00000000, а таблица векторов прерываний как-раз начинается с адреса 0x00000000.
_start: /* заполняем таблицу векторов прерываний переходами на наши обработчики Таблица векторов прерываний: Reset - после сброса Undefined instruction SWI - программное прерывание Prefetch Abort - ошибка обращения к памяти при выборке команды Data Abort - ошибка обращения к памяти при доступе к данным IRQ FIQ - быстрое прерывание */ b reset b loop b loop b loop b loop nop /* сюда будет помещена сигнатура таблицы векторов прерываний */ b loop b loopКак видно, сразу же после сброса будет запущена функция reset. В этой функции и производятся все необходимые действия по инициализации микроконтроллера, а именно:
- Инициализация PLL (по русски - ФАПЧ - схема фазовой автоподстройки частоты. Она увеличивает частоту сигнала, полученного от внешнего генератора, позволяя процессору работать на более высокой частоте, чем может дать кварц).
- Инициализация делителя VPB (делит тактовый сигнал от процессора для медленных устройств на шине VPB).
- Инициализация MAM - Memory Accelerator Module (поскольку запуск программы напрямую из флеш-памяти будет ограничивать производительность кристалла, а системный кэш является слишком сложным устройством для ядра ARM (чей девиз - "простота"), то используется такой модуль - нечто среднее между кэшем и запуском программ напрямую из памяти).
- Настройка стеков для всех режимов работы ARM (микроконтроллер ARM имеет несколько режимов работы и в каждом используется свой стек - соответственно нужно настроить Stack Pointer для каждого из режимов).
Инициализация PLL
Инициализация PLL проходит в две стадии - вначале мы устанавливаем необходимые для работы блока переменные, а затем ждем когда блок войдет в стабильный режим работы, после чего подключаем его к процессору.
Работа PLL определяется двумя константами:
- M = C_clk / F_osc, где C_clk - желаемая частота работы CPU, а F_osc - частота кварца.
- Значение P выбирается из уравнения: 156 < 2 * C_clk * P < 320
ldr r0, PLLBASE mov r3, #PLLCFG_VAL str r3, [r0, #PLLCFG_OFFSET] mov r3, #PLL_ENABLE str r3, [r0, #PLLCON_OFFSET] mov r1, #PLLFEED_1 mov r2, #PLLFEED_2 str r1, [r0, #PLLFEED_OFFSET] str r2, [r0, #PLLFEED_OFFSET] wait_pll_lock: /* ждем установления стабильного сигнала */ ldr r3, [r0, #PLLSTAT_OFFSET] tst r3, #PLLSTAT_LOCK beq wait_pll_lock mov r3, #PLL_ENABLE_CONNECT str r3, [r0, #PLLCON_OFFSET] str r1, [r0, #PLLFEED_OFFSET] str r2, [r0, #PLLFEED_OFFSET]Инициализация делителя VPB
Вся настройка заключается лишь в установке делителя, определяющего величину частоты для VPB. Делить частоту процессора можно на 2, на 4 или же вовсе не трогать. Определяется это двумя первыми битами в регистре VPBDIV:
- 00 - делить на 4
- 01 - не трогать
- 10 - делить на 2
ldr r0, VPBBASE mov r3, #VPB_DIV_VAL str r3, [r0]Инициализация MAM
Вся настройка MAM заключается в выборе режима работы MAM - управление через первые два бита регистра MAMCR:
- 00 - MAM отключен
- 01 - MAM частично включен - разрешена лишь предвыборка команд, данные приходят напрямую в процессор
- 10 - MAM полностью включен
ldr r0, MAMBASE mov r3, #MAMCR_VAL str r3, [r0, #MAMCR_OFFSET] mov r3, #MAMTIM_VAL str r3, [r0, #MAMTIM_OFFSET]
Настройка стеков
Ну тут все просто - переходим поочередно в каждый режим работы процессора, устанавливаем SP, вычисляем адрес стека для следующего режима, а поскольку эта операция у нас последняя - в конце мы переходим к исполнению функции main().
ldr r0, STACK_START msr CPSR_c, #FIQ_MODE|IRQ_DISABLE|FIQ_DISABLE mov sp, r0 sub r0, r0, #STACKSIZE msr CPSR_c, #IRQ_MODE|IRQ_DISABLE|FIQ_DISABLE mov sp, r0 sub r0, r0, #STACKSIZE msr CPSR_c, #SVR_MODE|IRQ_DISABLE|FIQ_DISABLE mov sp, r0 sub r0, r0, #STACKSIZE msr CPSR_c, #UND_MODE|IRQ_DISABLE|FIQ_DISABLE mov sp, r0 sub r0, r0, #STACKSIZE msr CPSR_c, #ABT_MODE|IRQ_DISABLE|FIQ_DISABLE mov sp, r0 sub r0, r0, #STACKSIZE msr CPSR_c, #USR_MODE mov sp, r0 /* GOTO main() */ b main
main.c
В функции main() производится лишь одно действие - подача высокого уровня напряжения на ногу P0.0.
Поскольку число ног у микроконтроллера ограничено, а действий, которые можно повесить на ноги, весьма много, каждая нога может использоваться для нескольких разных функций, например: как отдельный проводник в адресной шине, как один из выводов UART'а или как вывод общего назначения. Функция, исполняемая ногой МК определяется при помощи регистров PINSEL0, PINSEL1, PINSEL2.
В нашей функции main() мы настраиваем выводы с P0.0 по P0.15 как выводы общего назначения - в этом режиме можно просто подать на определенный вывод высокое или низкое напряжение или считать, какой уровень напряжения сейчас на ноге - 0 или 1.
Для каждого из выводов общего назначения, существует набор управляющих регистров. Мы используем только три регистра и только для порта P0:
- IODIR0 - определяет режим работы для каждой ноги: 0 - на выход (выдаем значение на ногу), 1 - на вход (считываем значение с ноги).
- IOCLR0 - единица в соответствующем разряде регистра устанавливает низкий уровень напряжения на соответствующем выводе. Нули в разрядах не производят никакого эффекта.
- IOSET0 - единица в соответствующем разряде установит высокий уровень напряжения на соответствующем выводе. Нули в разрядах не производят никакого эффекта.
Если были установлены единицы в соответствующих разрядах регистров IOSET и IOCLR, то будет выполнено действие, ассоциированное с тем регистром, который изменили последним.
#include "lpc2292.h" int main(void) { /* Настраиваем P0 как GPIO */ PINSEL0 = PINSEL0 | 0x00000000; /* Настриваем P0 как output порт */ IODIR0 = IODIR0 | 0xffffffff; /* Гасим все */ IOCLR0 = 0xffffffff; /* Зажигаем P0.0 */ IOSET0 = 0x00000001; while(1) {} }
Компиляция
Для компиляции необходим ARM-toolchain - набор из gcc и сопутствующих утилит. Как его собрать я уже описывал в своем июньском посте.
Также необходим скрипт линковщика, в котором описано как собирать объектные файлы под архитектуру ARM7. Лично я утянул скрипт отсюда, но если есть желание, можно попробовать написать его самостоятельно.
Makefile:
CC=arm-elf-gcc LD=arm-elf-ld CP=arm-elf-objcopy CFLAGS=-c -Wall -Wstrict-prototypes -Wno-trigraphs -O0 -pipe -g -mcpu=arm7tdmi LDFLAGS=-nostartfiles -N -p -T./lpc2292.ld SRC=startup.s main.c OBJ=startup.o main.o BIN=helloworld all: objs $(LD) $(LDFLAGS) $(OBJ) -o $(BIN) # convert from ELF format to Intel HEX format # $(CP) -O ihex $(BIN) $(BIN).hex objs: $(SRC) $(CC) $(CFLAGS) $(SRC) .PHONY: clean clean: rm -f $(OBJ) \ $(BIN) \ $(BIN).*
Скачать исходный код hello-world'а можно отсюда.
Также, рекомендую ознакомиться с книгой "Микроконтроллеры ARM7. Семейство LPC2000 компании Philips. Вводный курс" за авторством Мартина Тревора и с документацией по чипам 2292/2294 вот тут.