Линукс и контроллеры серии LPC2xxx

Микроконтроллеры Philips серии LPC2xxx — это процессоры с ядром ARM7-TDMI-S, оснащенные большим количеством встроенных устройств. Они очень маленькие (LPC расшифровывается Low Pin Count, т. е. микросхемы с маленьким числом ножек), дешевые (3—5$) и быстрые, как и все ARM7; рабочая частота процессора при включенном PLL может достигать 60 МГц. Последнее обстоятельство позволяет делать на них почти все, что угодно, особенно учитывая, что у этих микросхем на борту от 16 до 32 Кб оперативной памяти и 128—256 Кб флеша. У них есть, конечно, и недостатки. Открываешь errata и видишь, что из всего ошеломляющего разнообразия функций то не работают, в этом ошибка, не работает режим Thumb, глючит при арбитраже CAN-контроллер, нельзя по-человечески установить режим внешних прерываний..., но это, конечно, проблема не только филипса.

В различных модификациях встроены разные периферийные устройства. Лично я работал с LPC2119, в котором были: два контроллера сети CAN, два последовательных порта, 3 раза по 32 ноги ввода/вывода, АЦП, часы, несколько таймеров, какой-то генератор импульсов PWM, шина I2C, еще одна шина SPI — и это один из самых дешевых вариантов.

Прошивку с программой можно заливать во флэш через RS-232 (а также через JTAG, но это неудобно). Для программирования этих процессоров обычно пользуются средой разработки IAR, которая работает под Windows. Либо Keil Systems, которая тоже работает под Windows, но при этом использует компилятор gcc. Мне в силу особенностей характера эти варианты не подошли, и я программировал данный процессор в Линуксе.

Компиляция

Благодаря усилиям фирмы CodeSourcery, в которой числится добрая половина всех основных разработчиков GCC, этот свободный компилятор поддерживает весь набор архитектур ARM7, в том числе и ARM7-TDMI-S, используемый в LPC2xxx. GCC под ARM7 можно собрать самому из исходников, либо скачать c их сайта готовый набор из самого компилятора и binutils, в состав которых входят ассемблер as, линкер ld и программы objdump и objcopy. Это называется GNU Toolchain и располагается здесь. На момент написания там была версия только под 32-разрядный Линукс, но она прекрасно работала и на 64-битной системе. Для установки достаточно распаковать архив, скажем, в директорию /opt, и добавить ее в PATH, но последнее не обязательно. Команда такая:

tar xjf arm-2006q3-27-platform.tar.bz2 -C /opt

Теперь можно компилировать программы на Си для архитектуры ARM. Кстати говоря, gcc поддерживает не только седьмой, но и ARM9, а это уже совсем серьезная платформа.

/opt/arm/bin/arm-none-eabi-gcc -c hello.c

Ключ -c заставляет gcc не вызывать линкер и выдавать объектный файл hello.o, процедуре доработки которого посвящен остаток текста.

Особенности архитектуры

Во-первых gcc лучше всего запускать с такими опциями: -nostartfiles -mcpu=arm7tdmi-s -mlittle-endian -mno-long-calls -mno-thumb-interwork -ffreestanding -Wall. Это значит отказаться от подлинковки стандартных процедур инициализации libc, т. к. все равно на голом железе нет никакой библиотеки, выбрать нужную архитектуру проц. ядра, чтобы компилятор генерировал более эффективный код, и включить все возможные варнинги компилятора (это хорошее лекарство от забывчивости). Пару слов о качестве кода. Все эти опции тут не особенно-то причем. В то время как IAR'овский компилятор, в котором нет режима оптимизации, генерирует вполне себе ничего код, достаточно компактный, GCC при отключенной оптимизации делает какой-то ужас. Оптимизация включается опцией -O3, но с ней есть несколько проблем.

Оптимизация

Доступ к разным регистрам LPC обычно рекомендуется делать, определяя адрес в виде разыменования константного указателя на volatile:

#define U0THR  (*((volatile _u32*) 0xE000C000))

И затем обращаясь к нему как к обычной переменной:

int u = U0THR;
U0THR = 0xCAFECAFE;

С GCC такой фокус не проходит. При выключенной оптимизации все работает как надо, но с -O3 наоборот. Считается, что GCC сильно глупеет, видя в тексте слово volatile, и генерирует плохой и неэффективный код. Будь оно даже так, это само по себе еще не беда, не начни он безжалостно выбрасывать из кода бессмысленные, с его точки зрения, обращения к регистрам. Программы после такой оптимизации, естественно, не работают. Короче говоря, все происходит так, как будто слова volatile не было бы вообще. Но есть способ перехитрить GCC, его придумали разработчики ядра, он называется барьеры оптимизации. Об нем написано в руководстве по gcc в разделе про встраиваемый ассемблер: если указать в клоббер-листе (списке затираемых данных) параметр "memory". То есть непосредственно вот так:

volatile __asm__("":::"memory");

Все данные, записанные до этой строчки, будут записаны в свои ячейки, а все, читаемые после, -- прочитаны в обязательном порядке. Итак, для достижения нужного эффекта необходимо определить макрос barrier с приведенным выше содержанием, и разбросать его вызовы сразу после тех мест, где меняются данные, по смыслу являющиеся volatile. Для пущего удобства можно определить функции доступа к регистрам:

inline static _u32 readreg(_u32 *addr) 
{
        _u32 tmp; 
        barrier; 
        tmp = *addr; 
        barrier; 
        return tmp;
}

inline static void writereg(_u32 *addr, _u32 val) 
{
        barrier; 
        *addr=val; 
        barrier;
}

Как видно, здесь не без перестраховки. После таких вещей оптимизация на любых уровнях будет происходить безвредно и безболезненно.

Для LPC2119 я сделал заголовочный файл с определением регистров, его можно скачать.

Прерывания

Обработчики прерываний в ARM7 отличаются от обычных функций особенной процедурой вызова и возврата. GCC умеет автоматически генерировать код для этих случаев, если в прототипе функции-обработчика написать __attribute__ вот так:

static void isr()  __attribute__((interrupt("IRQ")));

Вместо IRQ можно написать FIQ, это немного другой случай.

Компоновка

Код скомпилировали, надо теперь из него сделать бинарный образ, который будет заливаться во флэш-память. Здесь не обойтись без линкера и скрипта для него, задача которого -- в явном виде определить адреса памяти в которых будет располагаться код.

MEMORY 
{
        flash (rx): org = 0x00000000, len = 0x00020000 /* 128k flash */
        ram (rw):   org = 0x40000000, len = 0x00004000 /* 16k static ram */
}

SECTIONS
{
        .text : 
        {
                start2119.o(.text)
                *.o(.text)
        } >flash
        .rodata : ALIGN(4)
        {
                *(.rodata*)
        } >flash
        . = ALIGN(4);
        _etext = . ;
        PROVIDE (etext = .);
        .data : AT(_etext) ALIGN(4)
        {
                _data = . ;
                *(.data)
                *(.fastcode)
        } >ram
        . = ALIGN(4) ;
        _edata = LOADADDR(.data) + SIZEOF(.data) ;
        PROVIDE ( edata = _edata);
        .bss : AT(_edata) ALIGN(4)
        {
                _bss = . ;
                *(.bss)
                *(COMMON)
        } >ram
        . = ALIGN(4);
        _ebss = LOADADDR(.bss) + SIZEOF(.bss);
        _end = _ebss;
        PROVIDE(end = _end);

        /DISCARD/ :
        {
                *(.comment)
                *(.ARM.attributes)
        }
}
    

Этот скрипт помещает все изменяемые данные в оперативную память, а константы и код во флеш. Адрес и размер областей флэш-памяти и RAM правится в самом начале. Вызов линкера с таким скриптом производится вот как:

arm-none-eabi-ld -T link.ld -o firmware.o \
start2119.o myfile1.o myfile2.o \
-L/opt/arm/lib/gcc/arm-none-eabi/4.1.0 -lgcc

Библиотека -lgcc содержит разные встроенные функции вроде целочисленного деления, без нее линковка не пройдет. В конце концов образуется файл firmware.o с четырьмя секциями: .text, .data, .rodata и .bss, в которых адреса уже настроены как надо. Так что надо просто извлечь эти секции из .o-файла формата ELF и превратить в бинарник. Специально для этого есть программа objcopy.

arm-none-eabi-objcopy -j .text -O binary firmware.o firmware.bin
arm-none-eabi-objcopy -j .rodata -O binary firmware.o firmware.rodata
arm-none-eabi-objcopy -j .data -O binary firmware.o firmware.data
cat firmware.rodata firmware.data >> firmware.bin

Секцию .bss мы не копируем, т. к. она все равно содержит неинициализированные данные и должна обнуляться при старте.

Код запуска

В скрипте компоновки и командной строчке ld есть файл start2119.o, о котором пока что не упоминалось. Это стартовый код, написанный на ассемблере, который должен располагаться по адресу 0x0 и отвечать за инициализацию секций, настройку стеков и т. д. В коммерческих средах IAR и Keil такие стартапы включаются в комплект, а я написал свой. Его можно скачать тут. Компилируется ассемблером arm-none-eabi-as. Все стеки, кроме пользовательского имеют размер 256 байтов и располагаются в конце операвной памяти. Пользовательский неограниченного размера, растет вниз. После всех инициализаций управление передается сишной функции main.

Программирование микросхемы

LPC2xxx программируется через встроенный загрузчик через ком-порт. Для этого дела у них существует специальный протокол и программа Philips Flash Utility под Windows. Для Линукса, впрочем, тоже есть отличная программка lpc21isp, которая держит этот протокол ничуть не хуже. Сайт у нее накрылся, но можно скачать исходник с моего. Компилируется безо всяких дополнительных библиотек. Получается файл lpc21isp, который можно запускать так:

sudo ./lpc21isp -bin firmware.bin /dev/ttyS0 19200 11509

Делать это лучше из-под рута, т. к. в разных линуксах разные права на доступ к ком-порту. Цифра 11509 это частота кварца на процессоре, а 19200 -- скорость порта, у меня не работало на скоростях больше, чем 19200. Я делаю специальный таргет в мейкфайле и просто пишу make prog, после чего моя прошивка автоматически собирается и записывается в устройство.

Впрочем, если залить в процессор файл firmware.bin, полученные описанным выше способом, он не запустится, потому что в нем нет контрольной суммы. Для ее простановки нужно воспользоваться еще одной маленькой программкой. Она патчит непосредственно сам файл .bin.

Теперь вроде бы все должно работать.

Ссылки

Mon Apr 2 23:06:05 2007