19 авг. 2010 г.

Пишем Hello-world для микроконтроллера архитектуры ARM7.

В этом посте я опишу как написать и скомпилировать Hello-world для микроконтроллера, входящего в состав SDK2.0 - LPC2292 от NXP Semiconductors. Естественно "Hello world!" будет аппаратный - мы подадим высокий уровень напряжения на одну из ног микроконтроллера.
Мое описание не будет изобиловать сложными техническими подробностями - за ними лучше всего обратиться к полезным ссылкам в конце статьи. Пост подойдет для людей, которые хотят просто взять и написать свою первую программу для 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
После изменения констант (регистр PLLCFG) или включения/подключения PLL к CPU (регистр PLLCON) нужно последовательно записать значения 0xaa и 0x55 в регистр PLLFEED для принятия изменений.
    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 полностью включен
... и в задании количества тактов тактового сигнала процессора, которое требуется для обращения к Flash-памяти (регистр MAMTIM, первые три бита). User manual от NXP рекомендует использовать значение 001 для систем, работающих на частотах меньше 20 MHz, 010 для частоты от 20 до 40 MHz. И, наконец, 011 и выше для систем работающих на частотах больше чем 40 MHz.
    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 вот тут.