Pull to refresh

Первые шаги со Stellaris LM4F120 launchpad evaluation board

Reading time 9 min
Views 28K
Завалялась у меня в столе плата Stellaris LM4F120, с которой я решил, наконец, разобраться. Писать будем программу, которая включает-выключает светодиоды, установленные на плату в ответ на нажатия имеющихся на плате кнопок.

Сразу скажу, с электроникой у меня туговато, поэтому в статье будет высказано мое мнение по поводу того, как оно работает, которое может не совпадать с реальностью. С терминологией у меня совсем плохо, поэтому если знаете правильный термин — поправляйте, пожалуйста.

Подготовка

Для начала, надо установить необходимое ПО для разработки:
  • Компилятор и среда разработки
  • SDK (StellarisWare)
  • Драйвера отладочного модуля
  • Программу для прошивки
  • Документация


С компиляторами и IDE Texas Instruments предоставляет выбор: их собственный Code Composer Studio, основанный на Eclipse (можно установить как отдельно, так и как плагин к уже установленному Eclipse, если таковой имеется), IAR Embedded Workbench, Keil и Mentor Graphics Sourcery CodeBench:


Бесплатная версия CCS полнофункциональна и я буду пользоваться именно ей. По вышеприведенной ссылке можно скачать «CD», скрывающийся за part number'ом EK-LM4F120XL-CCS, на котором будет все, что нужно для разработки, но содержимое архива несколько устарело, и, похоже, не обновляется. Но можно скачать все по отдельности:

Code Composer Studio, StellarisWare скрывается за part number'ом SW-EK-LM4F120XL здесь, Stellaris ICDI drivers, LMFlash programmer, Evaluation board User's Manual, Datasheet. После установки StellarisWare в каталоге, куда его установили можно будет найти подкаталог docs с документацией. Там основной интерес представляет файл SW-DRL-UG-9453.pdf (цифры могут отличаться в зависимости от версии SW) — документация по функциям, предоставляемым SDK (также, если заглянуть в SW-EK-LM4F120XL-UG-9453.pdf, можно найти высокоуровневые функции для работы сразу со светодиодами и кнопками, но это не путь настоящего джедая: мы же хотим понимать, что происходит внутри?)

Надо сказать, что у TI произошло некоторое переименование, и импользуемый в плате микроконтроллер LM4F120H5QR стал называться TM4C1233H6PM, соответственно, на данный момент для поисков информации следует использовать второе наименование.

На сайте Texas Instruments можно найти видеолекции про плату и печатные материалы к ним

Подключаем плату к компьютеру:

подключаем USB-кабель в порт отладчика, убеждаемся, что переключатель питания (рядом с USB-портом) находится в положении «debug». Если все сделали правильно, кроме зеленого светодиода питания должен загореться светодиод рядом с кнопкой «reset», и через 5 секунд начать переливаться разными цветами (если, конечно, вы не успели прошить что-то отличное от заводской прошивки).

Замечение про Windows 8.1. Эта операционная система по умолчанию не позволяет загружать неподписанные драйвера, что приводит к невозможности установки драйверов ICDI. Лечится это следующим образом: нажимаем Win+I→Power, зажимаем Shift и щелкаем по Restart, ждем, появится меню с вариантами починки, выбираем Troubleshoot→Advanced options→Startup settings→Restart. После перезагрузки появится меню, в котором надо выбрать «Disable driver signature enforcement».

Самая первая программа


Итак, мы установили все необходимое. Пора писать. Создаем проект в CCS: File→New→CCS Project. Настраиваем аналогично картинке:

Получим проект, в котором будут сразу два файла с исходниками: пустой main.c и lm4f120h5qr_startup_ccs.c. Второй файл содержит boilerplate код для инициализации таблицы прерываний и функции-заглушки для обработки прерываний.

Сразу настроим пути для поиска заголовочных файлов и библиотек, идем в меню Project→Properties, далее:


Пути, естественно, замените на свои.

Добавляем #include в main.c и проверяем, что все компилируется:
#include <inc/hw_types.h>
#include <inc/hw_gpio.h>
#include <inc/hw_memmap.h>
#include <driverlib/sysctl.h>
#include <driverlib/gpio.h>

int main(void)
{
	return 0;
}


Если не работает, чиним, если работает, можно приступать делать что-то полезное.

Рабочая частота


Первым делом, надо настроить частоту, на которой будет работать наш микроконтроллер. Максимальная частота, с которой он может работать — 80 МГц, но можно и меньше. Раздел 5.2.5 data sheet'а рассказывает нам как настроить частоту и пугает рисунком 5-5:

На данный момент, нас интересует выход «System clock». Согласно data sheet'у, в качестве источника тактовых импульсов могут быть:
  • Precision Internal OSC 16 МГц
  • Main OSC (на данной плате обозначен как Y1, и имеет частоту 16 МГц)
  • Internal OSC 30 кГц
  • Hibernation OSC (32.768 кГц) — Y2 на плате
  • PLL
  • Precision Internal OSC через делитель на 4


Из указанных источников два являются внешними, по отношению к микроконтроллеру (Main OSC и Hibernation OSC), остальные находятся внутри него.

Делитель, обозначенный на схеме как SYSDIV, может быть настроен на 1x–64x уменьшение частоты. Если в качестве источника используется PLL, дополнительно может быть задействован делитель частоты на 2, управляемый битом DIV400 (по умолчанию задействован). Для функционирования PLL требуется задающий генератор импульсов. Его частота может варьироваться в широких пределах, но ее надо указать при инициализации. Использование PLL — единственный способ завести микроконтроллер на максимальной частоте, поэтому использовать будем его (хоть и для наших целей такая бешеная частота не нужна). Для тактирования PLL может использоваться как внешний кристалл, так и внутренний.

StellarisWare предлагает функцию SysCtlClockSet для настройки всего, что имеет отношение к System clock одним махом. Для этого ей на вход подается куча флагов:
Для начала определимся используем мы PLL (SYSCTL_USE_PLL) или что-то другое (SYSCTL_USE_OSC) — флаг BYPASS на схеме.
Если используем PLL, надо выбрать кто будет задающим генератором: Main OSC (SYSCTL_OSC_MAIN) или Precision Internal OSC (SYSCTL_OSC_INT). Если используем Main OSC, надо указать его частоту (в нашем случае SYSCTL_XTAL_16MHZ).
Если не используем PLL, надо указать источник:
  • Precision Internal OSC — SYSCTL_OSC_INT
  • Precision Internal OSC через делитель на 4 — SYSCTL_OSC_INT4
  • Main OSC — SYSCTL_OSC_MAIN
  • Internal OSC — SYSCTL_OSC_INT30
  • Hibernation OSC — SYSCTL_OSC_EXT32


И, наконец, надо настроить делитель. Для этого есть макросы от SYSCTL_SYSDIV_1 до SYSCTL_SYSDIV_64. Если используется PLL, дополнительно можно использовать макросы SYSCTL_SYSDIV_2_5(/2.5) – SYSCTL_SYSDIV_63_5(/63.5). По неизвестным для меня причинам, для вызова SysCtlClockSet частота PLL считается равной 200 МГц, т.е. для того, чтобы работать на частоте 80 МГц надо указывать делитель 2.5.

Итого, вызов SysCtlClockSet может выглядеть так:
	// используем PLL, задающий генератор - Main OSC с частотой 16 МГц, используем делитель 2.5, итоговая частота 80 МГц
	SysCtlClockSet(SYSCTL_USE_PLL | SYSCTL_SYSDIV_2_5 | SYSCTL_XTAL_16MHZ | SYSCTL_OSC_MAIN);

или так:
	// используем PLL, задающий генератор - Precision Internal OSC, используем делитель 2.5, итоговая частота 80 МГц, отключаем Main OSC
	SysCtlClockSet(SYSCTL_USE_PLL | SYSCTL_SYSDIV_2_5 | SYSCTL_OSC_INT | SYSCTL_MAIN_OSC_DIS);

Или как вам заблагорассудится.

Моргаем светодиодами


Пришла пора сделать программу, результат работы которой можно увидеть. Документация на плату имеет схему включения светодиодов:


Т.е. светодиоды подключены к ногам 1–3 GPIO порта F (если кто знает, зачем резистор между базой и эмиттером транзистора — просветите, пожалуйста, и почему у него номинал не указан?). Порт GPIO имеет восемь ног, каждая из которых индивидуально может быть настроена как вход или как выход. Запись и чтение осуществляется со всех ног разом путем записи или чтения регистра GPIODATA, отображенного в память (соответственно читаем/пишем один байт — состояние всех ног). Доступ к порту может осуществляться двумя способами: посредством Advanced Peripheral Bus (APB) или через Advanced High-performance Bus (AHB). Первый способ «старый» и медленный, второй — современный и быстрый. Честно говоря, какая между ними разница с точки зрения программирования я не знаю (как минимум отличаются базовые адреса для портов), я пользовался APB, базовый адрес для порта F — 0x40025000.

Итак, чтобы зажечь светодиод надо выставить высокий уровень напряжения на соответствующей ноге. Т.е. записать 1 в соответствующий бит. Обычно, чтобы поменять состояние какого-то бита, надо прочитать состояние регистра, установить значения нужных битов и записать полученное значение обратно. Операции с памятью медленные, поэтому разработчики микроконтроллера предоставляют возможность обойтись одной записью (без предварительного чтения): доступ к регистру осуществляется не по одному адресу, а по диапазону длиной 1024 байта, начинающемуся с базового адреса. В битах 2–9 адреса помещается маска битов, значения которых надо обновить. Т.е. если мы хотим записать 1 в бит №1, надо взять соответствующую маску: 0x02, сдвинуть ее влево на 2 бита и прибавить полученное значение к базовому адресу — получится адрес, по которому надо записать нужное состояние порта (0x02 или 0xFF — все равно будет взят только первый бит).

Функция выставления нужных бит могла бы выглядеть так:

void pinWrite(unsigned int base, unsigned char pins, unsigned char value)
{
	*((unsigned char *)base + ((unsigned int)pins << 2)) = value;
}


StellarisWare предоставляет функцию GPIOPinWrite с такой же сигнатурой, которая делает то же самое.

Не хватает только одного: надо включить порт и настроить ноги, к которым подключены светодиоды как выходы. Для желающих обойтись без SDK процесс описан в разделе 10.3 data sheet'а микроконтроллера. С помощью StellarisWare это делается вызовами SysCtlPeripheralEnable и GPIOPinTypeGPIOInput/GPIOPinTypeGPIOOutput.

#include <inc/hw_types.h>
#include <inc/hw_gpio.h>
#include <inc/hw_memmap.h>
#include <driverlib/sysctl.h>
#include <driverlib/gpio.h>

const unsigned int LED_RED = 0x02;
const unsigned int LED_GREEN = 0x08;
const unsigned int LED_BLUE = 0x04;

const unsigned int LEDS_ALL = 0x02 | 0x08 | 0x04;

int main(void)
{
	SysCtlClockSet(SYSCTL_USE_PLL | SYSCTL_SYSDIV_2_5 | SYSCTL_XTAL_16MHZ | SYSCTL_OSC_MAIN);

	SysCtlPeripheralEnable(SYSCTL_PERIPH2_GPIOF);
	SysCtlDelay(2);
	GPIOPinTypeGPIOOutput(GPIO_PORTF_BASE, LEDS_ALL);

	const unsigned long int delay = 80000000 / 3 / 2; // 80 МГц, 3 такта на единицу задержки, 1/2 секунды нужная задержка

	while (1)
	{
		GPIOPinWrite(GPIO_PORTF_BASE, LEDS_ALL, LEDS_ALL);
		SysCtlDelay(delay);
		GPIOPinWrite(GPIO_PORTF_BASE, LEDS_ALL, 0);
		SysCtlDelay(delay);
	}
	return 0;
}


Функция SysCtlDelay делает n итераций пустого цикла, позволяя делать задержки нужной длины. Каждая итерация длится 3 такта процессора. Компилируем, запускаем (F11 в CCS, потом надо будет нажать F8, т.к. по умолчанию в начале программы стоит неявный брейкпоинт). Светодиод на плате должен начать моргать белым цветом с периодом в одну секунду. Если вместо LEDS_ALL третьим аргументом первого вызова GPIOPinWrite передать, например, LED_GREEN, то моргать будет зеленым.

Кнопки!



Две кнопки подключены к ногам 0 и 4 GPIO порта F по такой схеме:

Из схемы видно, что нажатие кнопки подключает ногу к земле, а в ненажатом состоянии нога никуда не подключена. Это плохо, при попытке чтения с неподключенной никуда ноги прочитаться может что угодно. Нам надо обеспечить подключение напряжения питания когда кнопка не нажата. В этом нам поможет подтяжка вверх (pull-up), которая, практически, подключает ногу к питанию через резистор, так что когда кнопка не нажата на нее будет приходить напряжение питания через подтяжку, а когда нажата, ток с нее будет уходить в землю, т.е. отпущенная кнопка будет читаться как единица в порту, нажатая — как ноль.

Итак, настраиваем ноги 0 и 4 на вход, включаем pull-up:
	GPIOPinTypeGPIOInput(GPIO_PORTF_BASE, GPIO_PIN_0 | GPIO_PIN_4);
	GPIOPadConfigSet(GPIO_PORTF_BASE, GPIO_PIN_0 | GPIO_PIN_4, GPIO_STRENGTH_2MA, GPIO_PIN_TYPE_STD_WPU);


Функция GPIOPadConfigSet позволяет выставить ток, который может выдать нога (2, 4 или 8 мА) и ее режим. Стоит отметить, что в микроконтроллере имеется защита от изменения настроек некоторых ног некоторых портов GPIO (тех, которые могут использоваться для порта JTAG/SWD (биты 0-3 порта C) или для входа немаскируемого прерывания — NMI (7 бит порта D и 0 бит порта F)): изменять настройки pull-up/pull-down для этих ног можно только если в регистре GPIOCR записана 1. В сам регистр GPIOCR можно писать только если в регистре GPIOLOCK записано специальное «магическое число».

Собираем все вместе:
#include <inc/hw_types.h>
#include <inc/hw_gpio.h>
#include <inc/hw_memmap.h>
#include <driverlib/sysctl.h>
#include <driverlib/gpio.h>

const unsigned int LEDS_ALL = GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3;

const unsigned int SW1 = GPIO_PIN_0;
const unsigned int SW2 = GPIO_PIN_4;

void crSet(unsigned int base, unsigned char value);

int main(void)
{
	int led = 2;
	SysCtlClockSet(SYSCTL_USE_PLL | SYSCTL_SYSDIV_2_5 | SYSCTL_XTAL_16MHZ | SYSCTL_OSC_MAIN);

	SysCtlPeripheralEnable(SYSCTL_PERIPH2_GPIOF);
	SysCtlDelay(2);

	GPIOPinTypeGPIOOutput(GPIO_PORTF_BASE, LEDS_ALL);
	GPIOPinTypeGPIOInput(GPIO_PORTF_BASE, SW1 | SW2);

	HWREG(GPIO_PORTF_BASE + GPIO_O_LOCK) = GPIO_LOCK_KEY_DD;
	crSet(GPIO_PORTF_BASE, 1);
	GPIOPadConfigSet(GPIO_PORTF_BASE, SW1 | SW2, GPIO_STRENGTH_2MA, GPIO_PIN_TYPE_STD_WPU);
	crSet(GPIO_PORTF_BASE, 0);
	HWREG(GPIO_PORTF_BASE + GPIO_O_LOCK) = 0;

	while (1)
	{
		unsigned int state = ~GPIOPinRead(GPIO_PORTF_BASE, SW1 | SW2);
		led = ((state & SW1) << 1) | ((state & SW2) >> 1);
		GPIOPinWrite(GPIO_PORTF_BASE, LEDS_ALL, led);
	}
	return 0;
}

void crSet(unsigned int base, unsigned char value)
{
	unsigned long v = (HWREG(GPIO_PORTF_BASE + GPIO_O_CR) & 0xFFFFFF00) | value;
	HWREG(base + GPIO_O_CR) = v;
}


Эта замечательная программа и плата за 12 баксов, наконец, позволяет делать то же самое, что и простая схемка из менее чем десятка деталей, стоимостью рублей 10, поздравляю!
Tags:
Hubs:
+22
Comments 18
Comments Comments 18

Articles