Disclaimer! Авторы не гарантируют истину в последней инстанции, во всяком случае из-за малоопытности в новой для них сфере. Если вы видите грубую техническую ошибку, очень просим вас сообщить о ней как можно скорее!
Свела как-то судьба начинающего программиста и начинающего электронщика вместе. И начали они творить. Сделали они сдесяток небольших игрушек-мишек с записывающими звуковыми модулями и поняли, что использовать однофункциональные платы не так уж и весело. И вот светлыми летними вечерами они собирались и думали, что бы им такого интересного сделать? Судьба помогла им и во второй раз: они нашли объявление о продаже отладочной платы STM32VLDiscovery в России и уже через неделю трясущимися руками распаковали посылку и «поморгали» светодиодами. От ощущения полной власти над крошечным устройством загорелись глаза и заработали
Глаза загорелись ещё больше, когда мы узнали о возможных способах применения STM32. Первым делом мы задумались об подключении всяческой периферии. На руках у нас был семисегментный ЖКИ от советских часов. Подключили, написали таймер, но успокоиться не могли…
На следующий день зашли в небольшой магазинчик при сервисном центре и были ошарашены от огромного количества телефонов на разбор: 3 стенда, полностью заполненных различными «хитами» среди сотовых телефонов прошлых лет. Всего за 80 рублей был куплен Siemens C55, впоследствии ставший нашим донором.
Кое-кав расковыряв корпус, мы достали ЕГО: монохромный LCD-экран LPH7999-4 с разрешением 102 на 64 пикселя. Состоит он из, собственно, ЖК-матрицы и контроллера PCF8812 (ссылка на даташит). Контроллер же состоит из DDRAM, в котором побитово в виде таблицы хранятся состояния пикселей (1), IO-буфера, нескольких генераторов тока и логических элементов. Все действия с LCD происходят непосредственно с помощью контроллера через 8 ног VDD, SCK, MISO, DC, SS, GND, VOUT, RES, из которых две замыкаются через конденсатор, а остальные подключаются к ногам нашего процессора.
Одновременно в наших головах проскочила мысль: «Что же это за непонятные обозначения такие и как этим можно вообще управлять?». С выражением полной печали и безысходности мы начали гуглить, постоянно натыкаясь на полные неизвестных терминов статьи. В итоге после нескольких дней в наших головах немного уложилась новая информация.
MOSI (или SIMO, SDO, DO, DOUT, SO, MTSR) Master Output Slave Input |
выход Master'а, который должен быть подключен к входам Slave'ов |
MISO (или SOMI, SDI, DI, DIN, SI, MRST) Master Input Slave Output |
вход Master'а, в который подаются выходы Slave'ов |
SCK (или SCLK, CLK) | подаваемая тактовая частота для «парсинга» битов из MISOMOSI |
SS (или CS) | выбор периферийного устройства, с которым мы будем работать. Если устройств, больше чем одно, то для работы с конкретным нужно ВЫКЛЮЧИТЬ ногу-перемычку на выбранном и ВКЛЮЧИТЬ на всех остальных устройствах. |
Итак, для управления нашим LCD мы должны использовать SPI — стандартизированный интерфейс для общения с периферией. Чтобы им воспользоваться, нужно понимать принцип работы и всю связанную с ним терминологию, а в частности названия и предназначения всех ног.
Интерфейс SPI предполагает, что у нас существует какое-то ОДНО устройство, которое будет всем управлять (Master) и множество управляемых периферийных устройств, таких как датчики, ЖКЖКИ, карты памяти, АЦПЦАП и т.д. (Slave). На нашем Master'е мы должны выбрать 3 ноги для приёмапередачи данных и n ног-перемычек, где n — количество подключаемых периферийных устройств. На Slave-устройствах ноги для приёмапередачи обычно определены заранее (если это, конечно, не ещё один процессор) и описаны в соответствующих даташитах.
Рассмотрим пример работы передачи и получения данных между абстрактным Master'а и одним абстрактным устройством Slave.
Примечание! SPI устроен следующим образом: при передаче данных от MOSI Master'а к MISO Slave'а происходит одновременная передача данных от MOSI Slave'а к MISO Master'а и наоборот. Таким образом, сигнал SCK один и тот же для MISO и MOSI, соответственно работают они одинаково.
При передаче одного байта задействованы выходы SCK и MOSI. Ны выходе SCK идут тактирующие импульсы (перепады напряжения от логического нуля до логической единицы). При передаче логической единицы на выходе напряжение ~3.3В, при передаче нуля, соответственно ~0В. Длительность состояния логического нуля и логической единицы равны и задаются программно. При передаче одного байта, на каждый бит приходится импульс. Таким образом, на выходе SCK при передаче байта мы можем увидеть восемь одинаковых «горбков». На выходе MOSI передается непосредственно наша информация. Например, если мы передаем 10000001, сигнал будет выглядеть как большая яма, а если 10011001, то как яма с выступом по середине. Как по отдельности работают оба выхода сейчас, думаю, ясно, теперь же расскажем о том, как они между собой согласованы.
В режиме простоя. Тот момент, когда ничего не передается, то есть в промежутке между передачей байтов или же до начала их передачи при включенном SPI. Логично было бы предположить, что при отсутстии каких бы то ни было операций на обоих входах будет 0. Но нет, в режиме простоя на MOSI напряжение логической единицы, на SCK либо логической единицы, либо нуля. Это состояние SCK мы можем выбирать сами.
В режиме передачи. Здесь нам предстоит выбрать, как будут согласованы импульсы портов SCK и MOSI. Для этого придется ввести несколько плохих слов:
Фронт — это переход из одного состояние в другое, то есть скачок напряжения от логической единицы к логическому нулю. На изображении импульса это вертикальные палочки.
Фронт бывает нарастающим и спадающим: нарастающий — переход от логического нуля к логической единице, спадающий — наоборот, от логической единицы к логическому нулю.
Фронт бывает также передний и задний: передний фронт — первый случившийся скачок после режима простоя, задний фронт — второй случившийся скачок после режима простоя.
Разработчик может выбрать для SCK режим простоя (логическая единица или ноль) и режим передачи (по переднему или заднему фронту). Итого, выходит 4 режима работы:
Режим 0 (00):
Режим простоя — логический ноль.
Передача по переднему фронту.
Так как мы выбрали передачу по переднему фронту, во время перехода от напряжения логического нуля до напряжения логической единицы на SCK, на MOSI произойдет передача бита.
Режим 1 (01):
Режим простоя — логический ноль.
Передача по заднему фронту.
Так как мы выбрали передачу по заднему фронту, то сначала идет передний нарастающий фронт, потом некоторое время держится напряжение логического нуля, потом идет задний спадающий фронт. После этого на MOSI произойдет передача бита.
Режим 2 (10):
Режим простоя — логическая единица.
Передача по переднему фронту.
Во время передачи идет импульс на SCK. Но он не нарастающий, в отличие от двух предыдущих режимов, а спадающий. Так как выше напряжения логической единицы напряжения быть не может, первый импульс идет «вниз». Именно во время этого перехода (ведь мы выбрали передний фронт) и происходит передача бита на MOSI.
Режим 3 (11):
Режим простоя — логическая единица.
Передача по заднему фронту.
Во время передачи идет импульс на SCK, сначала спадающий, потом нарастающий. В это время происходит переход на MOSI.
Обычно режим работы не обозначен в даташитах, но его легко получить, если изучить поведение MOSIMISO и SCK на каком-нибудь графике в даташите.
Так чем же мы можем управлять на нашем контроллере?
Во-первых, у нас есть память, которую контроллер отображает на ЖК-матрице.
Во-вторых, у нас есть каретка памяти с координатами X и Y
В-третьих, у нас есть сдесяток различных битов:
Бит PD - если 0, то контроллер включен, если 1 - то контроллер в спящем режиме Бит V - если 0, то после записи данных происходит сдвиг каретки по Х на единицу, иначе сдвиг по Y на 9 (т.е. сразу после записанного столбика) Бит H - если 0, то включен режим для работы с обычным набором инструкций, если 1 - то с расширенным Биты D и E отвечают за режим работы дисплея: 00 - все пиксели не горят 01 - все пиксели горят 10 - если состояние пикселя в памяти 1, то он горит, если 0, то не горит (нормальный режим) 11 - если состояние пикселя в памяти 1, то он НЕ горит, если 0, то горит (инверсия) Биты TC1 и TC0 отвечают за коэффициент температуры LCD 00 - коэффициент 0 01 - коэффициент 1 10 - коэффициент 2 11 - коэффициент 3 Биты S1 и S0 отвечают за множитель внутреннего питания, т.е. теоретически во сколько раз питание, поданное на VDD, будет отличаться от внутреннего питания 00 - в два раза больше 01 - в три раза больше 10 - в четыре раза больше 11 - в пять раз ботльше Биты Vop6-Vop0 отвечают за величину исходного внутреннего напряжения Биты BS2-BS0 отвечают за смещение системы
Теперь приведём возможные команды для управления. Каждая из них формируется ровно из 8 бит:
(в любом режиме инструкций) | ||||||||
установить регистры PD, V, H | 0 | 0 | 1 | 0 | 0 | PD | V | H |
(в режиме обычного набор инструкций) | ||||||||
установить регистры D, E | 0 | 0 | 0 | 0 | 1 | D | 0 | E |
установить координату X каретки () | 1 | X6 | X5 | X4 | X3 | X2 | X1 | X0 |
установить координату Y каретки () | 0 | 1 | 0 | 0 | Y3 | Y2 | Y1 | Y0 |
(в расширенном наборе инструкций) | ||||||||
установить регистр TC | 0 | 0 | 0 | 0 | 0 | 1 | TC1 | TC0 |
установить регистр S | 0 | 0 | 0 | 0 | 1 | 0 | S1 | S0 |
установить регистр BS | 0 | 0 | 0 | 1 | 0 | BS2 | BS1 | BS0 |
установить регистр V () | 1Vop5 | Vop6 | Vop5 | Vop4 | Vop3 | Vop2 | Vop1 | Vop0 |
Чтобы корректно инициализировать LCD, мы должны подать напряжение на VDD, отключить на RES, подождать 100 мкс и подать на RES снова. При ОТКЛЮЧЕНИИ питания на RES контроллер переходит в спящий режим, RAM гарантировованно не очищается, множество регистров получают своё дефолтное значение. Подробнее можно почитать на стр. 14 в даташите на контроллер.
После этого мы должны выключить SS (т.е. «выбрать» устройство для работы) и выключить DC (т.е. начать передачу команд) и передать с помощью SPI несколько инициализирующих команд:
- включить питание, а заодно выставить V=0 и H=1
- выбрать коэффициент температуры TC=11
- установить максимальное внутренне питание Vop=1111111
- включить множитель внутреннего питания S=01
- установить смещение системы BS=011
- включить режим обычного набора инструкций H=0, V=0, PD=0
- выбрать обычный режим работы дисплея D=1, E=0
После этого наш дисплей загорится и покажет нам рандомные пиксели, взятые из необнулённой RAM.
К сожалению, на контроллере нет MOSI (т.е. нет никакой обратной связи), поэтому если дисплей ничего не показывает, то чтобы убедится, работает ли хотя бы SPI, после вышеописанных команд нужно померить напряжение на 7 ноге LCD.
Опытным (и долгим) путём выяснено следующее: если дисплей ничего не показывает, а напряжение на 7 ноге есть, то это означает, что SPI работает и что дисплею недостаточно внутреннего напряжения и его нужно увеличить с помощью регистров Vop и S (поставить на максимумы, например). В нашем случае дисплей загорался при ~6В.
Теорию мы изучили и теперь перейдём к реализации. Есть два способа реализовать работу с SPI: сделать всё ручками при помощи управления ногами процессором (software spi) или же использовать «железную» реализацию (hardware spi), которая есть в нашей STM32. Я, например, не вижу смысла реализовывать интерфейс с помощью мощностей процессора, поэтому использую hardware spi.
Код писать и отлаживать будем в CooCox IDE:
- Запустим CoIDE и создадим новый проект
- Выберем нужные нам модули GPIO (для управления ногами), SPI (для управления SPI), RCC
- Напишем небольшой каркас для нашей будущей программы
#include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "stm32f10x_spi.h" void SPIInit(void) { } void GPIOInit(void) { } void LCDInit(void) { } int main() { SystemInit(); GPIOInit(); SPIInit(); LCDInit(); return 0; }
- Начнём с заполнения GPIO. Для этого с помощью даташита на STM32 мы должны узнать, где находятся ноги «железного» SPI. У нашей модели это PA5 (SCK), PA6 (MISO), PA7 (MOSI). Эти ноги могут быть как обычными ногами, так и ногами SPI, поэтому нам нужно явно указать предполагаемое назначение и задействовать их.
- Рассмотрим оставшиеся ноги:
VDD нога для подключения питания, подключается к произвольной ноге на STM32 (в нашем случае на LPH7999-4 предел подключения до 6.5V, а STM32 выдаёт 3.3V на каждую свою ногу) VOUT вывод внутреннего питания, подключается к земле экрана через конденсатор на х мФ. GND земля, см. VOUT RES нога для управления сбросом контроллера, подключается к произвольной ноге на STM32 DC нога, отвечающая за режим передачи данных в контроллер, подключается к произвольной ноге на STM32. Если на ноге нет напряжения, то контроллер LCD интерпретирует полученные данные как команду, а если есть, то как набор из 8 пикселей, которые запишутся столбиком в DDRAM относительно местонахождения каретки. SS см. выше, подключается к произвольной ноге на STM32 - Припаиваем SCK, MOSI к PA5 и PA7, а DC, VDD, RES и SS к произвольным ногам. У нас это PB0, PB1, PB2, PB3 соответственно.
- Пишем код:
#define SCK_Pin GPIO_Pin_5 #define SCK_Pin_Port GPIOA #define MOSI_Pin GPIO_Pin_7 #define MOSI_Pin_Port GPIOA #define DC_Pin GPIO_Pin_0 #define DC_Pin_Port GPIOB #define VDD_Pin GPIO_Pin_1 #define VDD_Pin_Port GPIOB #define RST_Pin GPIO_Pin_2 #define RST_Pin_Port GPIOB #define SS_Pin GPIO_Pin_3 #define SS_Pin_Port GPIOB void GPIOInit(void) { // включаем тактирование (=питание) на порты A, B и железный SPI1 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_SPI1, ENABLE); GPIO_InitTypeDef PORT; // выбрали ноги для настройки PORT.GPIO_Pin = SCK_Pin | MOSI_Pin; // установили наименьшую скорость (максимальная скорость контроллера 4 Мбита в секунду) PORT.GPIO_Speed = GPIO_Speed_2MHz; // (важно!) определяем предназначение ног. здесь - выбор "альтернативной функции" ног PORT.GPIO_Mode = GPIO_Mode_AF_PP; // настроили ноги в порту А GPIO_Init(GPIOA, &PORT); // выбрали ноги для настройки PORT.GPIO_Pin = DC_Pin | VDD_Pin | RST_Pin | SS_Pin; // установили скорость (тут - без разницы) PORT.GPIO_Speed = GPIO_Speed_2MHz; // предназначение - общее, выход PORT.GPIO_Mode = GPIO_Mode_Out_PP; // настроили ноги в порту B GPIO_Init(GPIOB, &PORT); }
Напишем вспомогательные процедуры для читабельности кода:
-
void PowerOn() { VDD_Pin_Port->ODR |= VDD_Pin; } void PowerOff() { VDD_Pin_Port->ODR &= ~VDD_Pin; } void ResetOn() { RST_Pin_Port->ODR |= RST_Pin; } void ResetOff() { RST_Pin_Port->ODR &= ~RST_Pin; } void DCOn() { DC_Pin_Port->ODR |= DC_Pin; } void DCOff() { DC_Pin_Port->ODR &= ~DC_Pin; } void SSOff() { SS_Pin_Port->ODR &= ~SS_Pin; } void SSOn() { SS_Pin_Port->ODR |= SS_Pin; }
- Теперь настроим SPI:
void SPIInit(void) { SPI_InitTypeDef SPIConf; // указываем, что используем мы только передачу данных SPIConf.SPI_Direction = SPI_Direction_1Line_Tx; // указываем, что наше устройство - Master SPIConf.SPI_Mode = SPI_Mode_Master; // передавать будем по 8 бит (=1 байт) SPIConf.SPI_DataSize = SPI_DataSize_8b; // режим 00 SPIConf.SPI_CPOL = SPI_CPOL_Low; SPIConf.SPI_CPHA = SPI_CPHA_1Edge; SPIConf.SPI_NSS = SPI_NSS_Soft; // установим скорость передачи (опытным путём выяснили, что разницы от изменения этого параметра нет) SPIConf.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; // передаём данные старшим битом вперёд (т.е. слева направо) SPIConf.SPI_FirstBit = SPI_FirstBit_MSB; // внесём настройки в SPI SPI_Init(SPI1, &SPIConf); // включим SPI1 SPI_Cmd(SPI1, ENABLE); // SS = 1 SPI_NSSInternalSoftwareConfig(SPI1, SPI_NSSInternalSoft_Set); }
- Напишем функцию отправки данных по SPI
void SPISend(uint16_t data) { SPI_I2S_SendData(SPI1, data); // отправили данные while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // ждём, пока данные не отправятся }
- Допишем инициализацию в соответствии с теорией
void LCDInit(void) { SSOff(); DCOff(); PowerOn(); ResetOff(); ResetOn(); SPISend(0x21); // включаем питание, устанавливаем сдвиг каретки, включаем режим расш. инстр. SPISend(0b1001); // устанавливаем трёхкратный множитель внутреннего питания SPISend(0xFF); // включаем максимальное внутреннее питание SPISend(0x06); // устанавлиаем температуру SPISend(0x13); // устанавливаем bias (смещение системы) SPISend(0x20); // ..., включаем режим обычных инструкций SPISend(0b1100); // включаем нормальный режим дисплея }
- Project — Build (или F7)
- Flash — Program Download
- Смотрим и радуемся :3
Здесь можно скачать готовый проект для CooCox
Конечно, удивить в 2013 году подключенным дисплеем к ARM-процессору сложно. Для нас, как для начинающих разработчиков, это — первый шаг к реализации своего проекта уникального «электронного браслета».
Мы не мечтаем выйти на рынок, мы просто хотим получить опыт, сделать функциональный, стильный и долгоработающий гаджет для себя, а заодно и рассказать о наших успехах и неудачах здесь.
Сейчас наше устройство умеет рисовать картинки на экране, предварительно переделанные в массив 8-битных «столбиков» с помощью этого скрипта на Python'е, требующего Python 2.7 и PIL.
Использование: photo.py file24bit.bmp > bytes.c
Некоторые иллюстрации были взяты с http://easystm32.ru/interfaces/43-spi-interface-part-1
Автор: 3ap