Часть 1 - Схемотехническое решение
Примечание.
Выборки исходного кода, размещенные в тексте описания, использовать в своих проектах не рекомендуется. В конце описания имеется ссылка на архив с исходными кодами.
Автор проекта разработал программный драйвер для управления TFT модулем и библиотеку графических функций. Программный драйвер, поддерживающий два режима работы и различные TFT модули, компонуется с библиотекой графических функций на этапе компиляции с помощью Си++ классов. Исходные коды имеют подробные комментарии автора, а в статье мы коснемся только основных моментов. В первой части статьи мы упомянули, что важным преимуществом адаптера является совместимость с интерфейсом внешней памяти XMEM. Именно с этого режима работы мы и начнем.
16-битный доступ к TFT модулю через интерфейс XMEM
Интерфейс XMEM предназначен для доступа микроконтроллера к внешней памяти с 16-битной шиной адреса и 8-битной шиной данных. Так случилось, что протокол доступа к внешней памяти достаточно близок к протоколу управления TFT модулями (на контроллерах с 16-разрядным i80-совместимом системным интерфейсом). Стоит заметить, что не со всеми TFT модулями можно работать через интерфейс XMEM, что связано с различным временем доступа и тайм-слотами контроллеров TFT панелей.
Для записи данных во внешнюю память Arduino выполняет следующие действия:
- Устанавливает на линии ALE лог. 1.
- 16-битный адрес выставляется на шину, где младшие 8 бит адреса мультиплексированы с линиями данных.
- Устанавливается лог. 0 на линии ALE.
- Запись данных осуществляется по нарастающему фронту на линии WR (из лог. 1 в лог. 0 и обратно в лог. 1).
Для нашего случая мы можем использовать эту последовательность для передачи 16 бит данных в рамках одной операции, включая управление линий RS (выбор регистра). Мы будем использовать младшие 8 бит из 16-битного адреса для передачи младших 8 бит данных для TFT модуля. Адресная линия A8 будет использоваться для удержания сигнала RS, а 8 линий данных используются для передачи старших 8 бит данных для TFT модуля. Адресный бит 15 необходимо установить в лог. 1, чтобы гарантировать отсутствие конфликтов с внутренним ОЗУ. Расположение выводов платы Arduino при работе с дисплеем через интерфейс XMEM изображено в Таблице 1.
Таблица 1. Назначение выводов платы Arduino при использовании интерфейса XMEM.
Arduino | Порт | Функция |
22 | PA0 | D0/D8 |
23 | PA1 | D1/D9 |
24 | PA2 | D2/D10 |
25 | PA3 | D3/D11 |
26 | PA4 | D4/D12 |
27 | PA5 | D5/D13 |
28 | PA6 | D6/D14 |
29 | PA7 | D7/D15 |
35 | PC2 | /RESET |
37 | PC0 | RS |
39 | PG2 | /CS |
41 | PG0 | /WR |
Инициализация интерфейса XMEM сводится к простой записи корректных значений в два конфигурационных регистра. Выводы интерфейса XMEM 30–34 (порты PC3 – PC7) освобождаются под линии ввода/вывода общего назначения:
inline void Xmem16AccessMode::initialise()
{
// установка вывода Reset
pinMode(RESET_PIN,OUTPUT);
digitalWrite(RESET_PIN,HIGH);
// установка регистров интерфейса XMEM
// освобождаем порты PC3-PC7
XMCRB=_BV(XMM1) | _BV(XMM2);
// включаем XMEM
XMCRA=_BV(SRE);
}
Первостепенным требованием для нас является производительность, поэтому функции записи данных и команд были написаны на ассемблере:
inline void Xmem16AccessMode::writeData(uint8_t lo8,uint8_t hi8) { // Представленная запись эквивалентна: // *reinterpret_cast <volatile uint8_t *>(0x8100 | lo8)=hi8; // Такой метод займет пять циклов тактовой частоты __asm volatile(" ldi r27,0x81n
t" " mov r26,%0
n
t" " st X,%1
n
t" :: "d" (lo8), "d" (hi8) : "r26", "r27"); }
На ассемблере связка двух регистров r26 и r27 образует 16-разрядный регистр X, который используется в функции инициализации интерфейса XMEM и функции записи 8-битных данных во внешнюю память. Кроме того, во встроенном ассемблерном коде регистры r26 и r27 можно свободно использовать, не опасаясь конфликтов с кодом генерируемым Си-компилятором. Тем не менее, эти регистры объявлены в “clobber list” в конце ассемблерного кода (clobber list – список регистров, которые будут модифицироваться в ассемблерном коде).
Метод записи команды аналогичен записи данных, за исключением сброса бита A9 (лог. 0):
inline void Xmem16AccessMode::writeData(uint8_t lo8,uint8_t hi8) { // Представленная запись эквивалентна: // *reinterpret_cast <volatile uint8_t *>(0x8000 | lo8)=hi8; // Такой метод займет пять циклов тактовой частоты __asm volatile(" ldi r27,0x80n
t" " mov r26,%0
n
t" " st X,%1
n
t" :: "d" (lo8), "d" (hi8) : "r26", "r27"); }
Чтобы убедиться в работоспособности этого режима работы потребуется логический анализатор. Автор использовал анализатор Ant18e (Рисунок 8).
![]() |
|
Рисунок 8. | Диаграмма сигналов интерфейса XMEM: младшие 8 бит данных, сигналы RS и WR. |
На диаграмме сигналов между двумя маркерами времени отчетливо видно, что микроконтроллер AVR генерирует импульс на линии WR ровно за один машинный цикл (за один такт).
Некоторые TFT модули, с которыми проводились эксперименты, не работали корректно через интерфейс XMEM ввиду отсутствия или очень коротких задержек в тайм-слотах, даже после введения в код циклов задержки передачи данных. Поэтому было решено разработать еще один режим работы, эмулирующий автоматическую работу интерфейса XMEM, и, как выяснилось после тестирования, не зря.
Режим 16-битного доступа к TFT модулю через линии ввода/вывода общего назначения (GPIO access mode)
Чтобы развеять мнение о низкой производительности такого интерфейса, сразу нужно заметить, что программный код был оптимизирован, что в некоторых случая привело к огромному увеличению производительности. В этом режиме мы программно эмулируем автоматическую работу интерфейса XMEM с помощью обычных портов ввода/вывода. Метод имеет свои преимущества и недостатки. Однократная операция записи данных становится несколько более медленной по сравнению с XMEM интерфейсом, однако при записи множества одинаковых данных алгоритм можно оптимизировать, создав соответствующую процедуру для TFT модулей, поддерживающих 16-битный режим. Другим преимуществом является то, что мы не ограничены выводами интерфейса XMEM, и TFT модуль можно подключить к обычной плате Arduino на микроконтроллере с 32 Кбайт встроенной памяти.
Рассматривать инициализацию интерфейса мы не будем, ввиду того, что код, который просто настраивает порты и направление передачи данных, не представляет большого интереса.
Определение выводов микроконтроллера для подключения адаптера осуществляется в шаблоне класса Gpio16LatchAccessModeXmemMapping, содержащего в качестве параметров имя порта и номера выводов, к которым подключен адаптер. Например, для использования тех же выводов, которые использовались при доступе через интерфейс XMEM, необходимо определить следующий тип:
struct Gpio16LatchAccessModeXmemMapping {
enum {
// Индексы портов, не путать с физическим адресом
PORT_DATA = 0x02, // PORTA
PORT_WR = 0x14, // PORTG
PORT_RS = 0x08, // PORTC
PORT_ALE = 0x14, // PORTG
PORT_RESET = 0x08, // PORTC
// индексы выводов портов, не путать с выводами Arduino
PIN_WR = PIN0,
PIN_RS = PIN0,
PIN_ALE = PIN2,
PIN_RESET = PIN2
};
};
Шаблон класса Gpio16LatchAccessMode представлен ниже.
template<typename TPinMappings> class Gpio16LatchAccessMode { protected: static uint8_t _streamIndex; static void initOutputHigh(uint8_t port,uint8_t pin); public: static void initialise(); static void hardReset(); static void writeCommand(uint8_t lo8,uint8_t hi8=0); static void writeCommandData(uint8_t cmd,uint8_t data); static void writeData(uint8_t lo8,uint8_t hi8=0); static void writeMultiData(uint32_t howMuch,uint8_t lo8,uint8_t hi8=0); static void writeStreamedData(uint8_t data); }; typedef Gpio16LatchAccessMode<Gpio16LatchAccessModeXmemMapping> DefaultMegaGpio16LatchAccessMode;;
Кроме того, вы можете видеть, что для простоты использования конкретно определяется псевдоним типа с помощью ключевого слова typedef.
Основное внимание необходимо уделить алгоритму, выполнящему однократную запись данных WriteData() и, особенно, алгоритму записи последовательности данных writeMultiData (). Сперва рассмотрим метод записи одного значения:
template<class TPinMappings> inline void Gpio16LatchAccessMode<TPinMappings>::writeData(uint8_t lo8,uint8_t hi8) { __asm volatile( " sbi %1, %5n
t" // ALE = HIGH " out %3, %7
n
t" // PORTA = lo8 " sbi %2, %6
n
t" // RS = HIGH " cbi %1, %5
n
t" // ALE = LOW " out %3, %8
n
t" // PORTA = hi8 " cbi %0, %6
n
t" // /WR = LOW " sbi %0, %6
n
t" // /WR = HIGH :: "I" (TPinMappings::PORT_WR), // %0 "I" (TPinMappings::PORT_ALE), // %1 "I" (TPinMappings::PORT_RS), // %2 "I" (TPinMappings::PORT_DATA), // %3 "I" (TPinMappings::PIN_WR), // %4 "I" (TPinMappings::PIN_ALE), // %5 "I" (TPinMappings::PIN_RS), // %6 "d" (lo8), // %7 "d" (hi8) // %8 ); }
Простой метод. Это так называемая программная реализация протокола (bit-bang) контроллера TFT панели (с i80-совместимой системной шиной) с дополнительным выполнением задачи программирования регистра-защелки. Такой подход очень распространен в графических библиотеках для прорисовки цветных блоков. При выполнении очистки экрана, рисовании прямоугольников или просто прямых линий все сводится к установке «выходного окна» на дисплее, а затем выполняется прорисовка точек.
Метод записи множества данных WriteMultiData () обеспечивает определенное преимущество в том, что вам необходимо только один раз установить данные и линию RS, а затем изменять состояние линии WR столько раз, сколько нужно для прорисовки графического блока точек. Посмотреть авторский вариант этого метода можно в Листинге 1.
Работает данный код следующим образом:
- Мы устанавливаем 16 бит данных и линию управления RS.
- Если при входе в процедуру были включены прерывания, то мы их отключаем, чтобы обработчик прерывания не изменил состояние портов.
- Читаем состояние порта, с которого осуществляется управление линией WR, и сохраняем два значения в памяти: одно с установленным битом WR, другое – со сброшенным битом WR.
- Если количество точек, которое нужно записать, менее 40, то переходим к пункту 7.
- С помощью последовательных операций out передаем данные о 40 точках (пикселях). Скорость записи достигает 8 млн. точек в секунду.
- Вычитаем значение 40 из счетчика точек и, если их все еще больше 40, то возвращаемся к пункту 5.
- Рассчитываем косвенный переход на выполнение оставшихся 39 операций вывода данных, которые заполнят оставшиеся пиксели, и используем для перехода команду ijmp.
- Если мы отключали прерывания, то восстанавливаем их.
Значение 40 было выбрано опытным путем, как компромисс между используемой Flash-памятью и производительностью. Работоспособность кода и недостаток программной реализации алгоритма наглядно демонстрируется на Рисунке 9 и 10.
![]() |
|
Рисунок 9. | Эффект оптимизации операций записи данных в TFT модуль. Скорость заполнения 8 млн пикселей в секунду, длительность импульса записи 120 нс. |
Безусловно, требуется дальнейшая ручная оптимизация, и в следующей части статьи мы познакомимся с некоторыми аспектами алгоритма оптимизации прорисовки линий, рассмотрим варианты конфигурирования программного драйвера для различных контроллеров TFT панелей.
Продолжение следует...
Часть 3 - Оптимизация алгоритма прорисовки линий и варианты конфигурирования драйвера