[Из песочницы] STM32 и LCD, быстрая заливка экрана
В настоящее время получили распространение различные жидкокристаллические дисплеи, которые отлично подключаются к контроллерам семейства STM32. В данной статье речь пойдет об одном из распространенных контроллеров STM32F103C8T6 и дисплее 7» на контроллере SSD1963. Оба в виде законченных узлов легко доступны на Aliexpress и относительно недорого стоят. Конечно, все рассмотренное ниже справедливо и для других дисплеев с параллельным интерфейсом и большинства контроллеров STM32.
Вот так выглядят соединяемые устройства:
В комплекте с дисплеем была распиновка выводов и код инициализации для 51-го контроллера с комментариями на языке изготовителя.
Коротко о подключении
Подключение дисплея заключается в подаче питания 3.3 и 5 вольт на нужные выводы и соединении информационных линий с контроллером. Управляющие сигналы D/C, WE, RST подключатся к свободным линиям ввода-вывода на процессоре. В нашем случае это D/C — PA1, WE — PA8, RST — PA2. Сигналы RD и CS можно не использовать, при этом на RD надо подать логическую единицу, т.е. подключить через резистор (в данном случае 4,7 КОм), а на CS — »0», т.е. подключить на землю.
Первоначально он так и работал. Однако, как показала проверка, если вы не хотите использовать шину данных для других целей, он не нужен.
Далее нужно подключить шину данных. У данного дисплея она предполагается 16-разрядная, но можно при инициализации выбрать 8-и и 9-и битные режимы работы. То есть нужно подключить как минимум линии дисплея DB0-DB7, как максимум еще и DB8-DB15. Для удобства программирования и минимизации команд преобразования данных лучше их завести на одну группу ввода-вывода. Если рассматривать вариант 16-разрядной шины данных, то выбирать на данном микроконтроллере не приходится — только PB0 — PB15.
Соединяем их соответственно с DB0-DB15 дисплея:
Конечно есть еще и PA0-PA15, но если мы хотим использовать ST-Link для отладки, то пара из них уже занята.
На гребенке дисплея остается много неподключенных контактов, пусть это вас не смущает. На нем присутствует слот SD карт памяти, сенсор экрана, даже есть разводка под микросхему EEPROM памяти, но сама она отсутствует. Эти устройства и занимают остальную часть разъема. Кстати, под 40-контактный разъем дисплея идеально подходит шлейф PATA жестких дисков компьютера.
Инициализация дисплея
Оригинальный код почти без изменений перенесен в проект, добавлена только условная компиляция для выбора разрядности шины данных (инициализация и команды идут по 8-битной шине, независимо от этого режима).
#define SET_LCD_RDS LCD_RDS_PORT->BSRR = LCD_RDS
#define RESET_LCD_RDS LCD_RDS_PORT->BRR = LCD_RDS
#define SET_LCD_WR LCD_WR_PORT->BSRR = LCD_WR
#define RESET_LCD_WR LCD_WR_PORT->BRR = LCD_WR
#define SET_LCD_RST LCD_RST_PORT->BSRR = LCD_RST
#define RESET_LCD_RST LCD_RST_PORT->BRR = LCD_RST
void SSD1963_Init (void)
{
uint16_t HDP=799;
uint16_t HT=928;
uint16_t HPS=46;
uint16_t LPS=15;
uint8_t HPW=48;
uint16_t VDP=479;
uint16_t VT=525;
uint16_t VPS=16;
uint16_t FPS=8;
uint8_t VPW=16;
RESET_LCD_RST;
delay_ms(5);
SET_LCD_RST;
delay_ms(5);
SSD1963_WriteCommand(0x00E2); //PLL multiplier, set PLL clock to 120M
SSD1963_WriteData(0x0023); //N=0x36 for 6.5M, 0x23 for 10M crystal
SSD1963_WriteData(0x0002);
SSD1963_WriteData(0x0004);
SSD1963_WriteCommand(0x00E0); // PLL enable
SSD1963_WriteData(0x0001);
delay_ms(1);
SSD1963_WriteCommand(0x00E0);
SSD1963_WriteData(0x0003);
delay_ms(5);
SSD1963_WriteCommand(0x0001); // software reset
delay_ms(5);
SSD1963_WriteCommand(0x00E6); //PLL setting for PCLK, depends on resolution
SSD1963_WriteData(0x0003);
SSD1963_WriteData(0x00ff);
SSD1963_WriteData(0x00ff);
SSD1963_WriteCommand(0x00B0); //LCD SPECIFICATION
SSD1963_WriteData(0x0000);
SSD1963_WriteData(0x0000);
SSD1963_WriteData((HDP>>8)&0X00FF); //Set HDP
SSD1963_WriteData(HDP&0X00FF);
SSD1963_WriteData((VDP>>8)&0X00FF); //Set VDP
SSD1963_WriteData(VDP&0X00FF);
SSD1963_WriteData(0x0000);
SSD1963_WriteCommand(0x00B4); //HSYNC
SSD1963_WriteData((HT>>8)&0X00FF); //Set HT
SSD1963_WriteData(HT&0X00FF);
SSD1963_WriteData((HPS>>8)&0X00FF); //Set HPS
SSD1963_WriteData(HPS&0X00FF);
SSD1963_WriteData(HPW); //Set HPW
SSD1963_WriteData((LPS>>8)&0X00FF); //Set HPS
SSD1963_WriteData(LPS&0X00FF);
SSD1963_WriteData(0x0000);
SSD1963_WriteCommand(0x00B6); //VSYNC
SSD1963_WriteData((VT>>8)&0X00FF); //Set VT
SSD1963_WriteData(VT&0X00FF);
SSD1963_WriteData((VPS>>8)&0X00FF); //Set VPS
SSD1963_WriteData(VPS&0X00FF);
SSD1963_WriteData(VPW); //Set VPW
SSD1963_WriteData((FPS>>8)&0X00FF); //Set FPS
SSD1963_WriteData(FPS&0X00FF);
SSD1963_WriteCommand(0x00BA);
SSD1963_WriteData(0x0005); //GPIO[3:0] out 1
SSD1963_WriteCommand(0x00B8);
SSD1963_WriteData(0x0007); //GPIO3=input, GPIO[2:0]=output
SSD1963_WriteData(0x0001); //GPIO0 normal
SSD1963_WriteCommand(0x0036); //rotation
SSD1963_WriteData(0x0000);
SSD1963_WriteCommand(0x00F0); //pixel data interface
#if DATAPIXELWIDTH==16
SSD1963_WriteData(0x0003); //16 bit (565)
#endif
#if DATAPIXELWIDTH==9
SSD1963_WriteData(0x0006); // 9 bit
#endif
#if DATAPIXELWIDTH==8
SSD1963_WriteData(0x0000); // 8 bit
#endif
delay_ms(5);
SSD1963_WriteCommand(0x0029); //display on
SSD1963_WriteCommand(0x00d0);
SSD1963_WriteData(0x000d);
}
void SSD1963_WriteCommand(uint16_t commandToWrite)
{
LCD_DATA_PORT->ODR = commandToWrite;
RESET_LCD_RDS;
RESET_LCD_WR;
SET_LCD_WR;
}
void SSD1963_WriteData(uint16_t dataToWrite)
{
LCD_DATA_PORT->ODR = dataToWrite;
SET_LCD_RDS;
RESET_LCD_WR;
SET_LCD_WR;
}
В коде нет инициализации портов ввода-вывода и системного таймера, на основе которого реализуются миллисекундные задержки (delay_ms ()).
После выполнения инициализации:
tick_init(); // инициализация системного таймера
lcd_port_init(); // инициализация портов ввода-вывода
SSD1963_Init(); // инициализация дисплея
Мы видим «мусор» видеопамяти на дисплее:
Заливка дисплея
Теперь хочется стереть этот мусор и залить экран каким-либо цветом. В исходнике от производителя необходимый материал для написания кода присутствует. Воспользуемся им.
// Fills whole screen specified color
void SSD1963_SetArea(uint16_t x1, uint16_t x2, uint16_t y1, uint16_t y2)
{
SSD1963_WriteCommand(0x002a);
SSD1963_WriteData((x1 >> 8) & 0xff);
SSD1963_WriteData(x1 & 0xff);
SSD1963_WriteData((x2 >> 8) & 0xff);
SSD1963_WriteData(x2 & 0xff);
SSD1963_WriteCommand(0x002a);
SSD1963_WriteData((y1 >> 8) & 0xff);
SSD1963_WriteData(y1 & 0xff);
SSD1963_WriteData((y2 >> 8) & 0xff);
SSD1963_WriteData(y2 & 0xff);
}
#if DATAPIXELWIDTH==16
void SSD1963_WriteDataPix(uint16_t pixdata)
{
LCD_DATA_PORT->ODR = pixdata;
SET_LCD_RDS;
RESET_LCD_WR;
SET_LCD_WR;
}
#endif
#if DATAPIXELWIDTH==9
void SSD1963_WriteDataPix(uint16_t pixdata)
{
LCD_DATA_PORT->ODR = (LCD_DATA_PORT->ODR & 0xfe00) | ((pixdata >> 8) & 0x000f) | ((pixdata >> 7) & 0x01f0);
SET_LCD_RDS;
RESET_LCD_WR;
SET_LCD_WR;
LCD_DATA_PORT->ODR = (LCD_DATA_PORT->ODR & 0xfe00) | ((pixdata << 1) & 0x01f7) | (pixdata & 0x0001);
RESET_LCD_WR;
SET_LCD_WR;
}
#endif
#if DATAPIXELWIDTH==8
void SSD1963_WriteDataPix(uint16_t pixdata)
{
LCD_DATA_PORT->ODR = (LCD_DATA_PORT->ODR & 0xff00) | ((pixdata >> 8) & 0x00f8) | ((pixdata >> 9) & 0x0004);
SET_LCD_RDS;
RESET_LCD_WR;
SET_LCD_WR;
LCD_DATA_PORT->ODR = (LCD_DATA_PORT->ODR & 0xff00) | ((pixdata >> 3) & 0x00fc);
RESET_LCD_WR;
SET_LCD_WR;
LCD_DATA_PORT->ODR = (LCD_DATA_PORT->ODR & 0xff00) | ((pixdata << 3) & 0x00f8) | ((pixdata << 2) & 0x0004);
RESET_LCD_WR;
SET_LCD_WR;
}
#endif
void SSD1963_ClearScreen(uint16_t color)
{
unsigned int x,y;
SSD1963_SetArea(0, TFT_WIDTH-1 , 0, TFT_HEIGHT-1);
SSD1963_WriteCommand(0x002c);
for(x=0;x
Как видно, код зависит от выбранной разрядности шины. Соответственно зависит и время, необходимое для выполнения передачи пикселя на дисплей. Для 16-разрядной шины пиксель передается за один цикл передачи по шине данных, для 9-разрядной — за два, для 8-разрядной — за 3. Откуда эти данные? Из документации на SSD1963.
В таблице можно найти расположение каждой составляющей цвета пикселя в зависимости от режима. В проекте используются режимы 8 бит, 9 бит и 16 бит (565 формат). Как видите, можно было также задействовать «чистый» формат 16 бит для более точного кодирования цвета, но он также требует трех циклов передачи данных по шине. Форматы 18 и 24 бит мы задействовать не можем по причине наличия только 16-битной шины на выходе дисплея.
Итак, с какой скоростью мы сможем заполнить дисплей на процессоре с тактовой частотой 72 МГц?
176 мс — 16-разрядная шина
374 мс — 9-разрядная шина
470 мс — 8-разрядная шина
Не очень быстро, конечно, но для отображения медленно меняющейся информации может быть достаточно. Более привлекательно выглядит конечно 16-разрядная шина, и возможно кому-то она и подойдет, но она занимает слишком много портов ввода-вывода, которых может потом не хватить для подключения других устройств к процессору.
Попробуем рассмотреть компромиссный вариант — 9 бит, как выигрывающий почти 0.1 с у 8-битного варианта за счет всего одного дополнительного порта ввода-вывода.
Оптимизация по скорости
Попробуем ускорить процесс заливки дисплея. Что если сократить количество логических операций внутри цикла?
// на входе 18-битный цвет RGB666
void SSD1963_WriteDataPix_9(uint32_t pixdata)
{
uint32_t tmp = (LCD_DATA_PORT->ODR & 0xfe00);
SET_LCD_RDS;
LCD_DATA_PORT->ODR = tmp | ((pixdata >> 9) & 0x01ff);
RESET_LCD_WR;
SET_LCD_WR;
LCD_DATA_PORT->ODR = tmp | (pixdata & 0x01ff);
RESET_LCD_WR;
SET_LCD_WR;
}
// на входе 18-битный цвет RGB666
void SSD1963_ClearScreen_9(uint32_t color)
{
unsigned int x,y;
SSD1963_SetArea(0, TFT_WIDTH-1 , 0, TFT_HEIGHT-1);
SSD1963_WriteCommand(0x002c);
for(x=0;x
Изменили кодировку цвета вместо 16-битной переменной в формате RGB565 используем 32-битную, задействовав только 18 из них в формате RGB666. Кроме того ввели временную переменную для хранения значения регистра LCD_DATA_PORT→ODR во время двух циклов вывода 9-битных данных на шину. Тут надо сделать оговорку, что это не всегда возможно, т.к. за время вывода состояние других портов группы GPIO B, настроенных на вывод, может быть изменено в это время в прерывании и программа будет работать неправильно. Однако в нашем случае таких проблем нет и мы проверяем чего мы достигли. Итак после первой оптимизации экран заполняется в 9-битном режиме за 298 мс. Если переменную не использовать, и работать с текущим состоянием порта, то прирост скорости тоже есть, хотя и не такой значительный — 335 мс:
void SSD1963_WriteDataPix_9(uint32_t pixdata)
{
SET_LCD_RDS;
LCD_DATA_PORT->ODR = (LCD_DATA_PORT->ODR & 0xfe00) | ((pixdata >> 9) & 0x01ff);
RESET_LCD_WR;
SET_LCD_WR;
LCD_DATA_PORT->ODR = (LCD_DATA_PORT->ODR & 0xfe00) | (pixdata & 0x01ff);
RESET_LCD_WR;
SET_LCD_WR;
}
Можно также ради скорости пожертвовать возможностью использования оставшихся портов группы B в режиме вывода и убрать логические операции, касающиеся сохранения их состояния:
void SSD1963_WriteDataPix_9(uint32_t pixdata)
{
SET_LCD_RDS;
LCD_DATA_PORT->ODR = pixdata >> 9;
RESET_LCD_WR;
SET_LCD_WR;
LCD_DATA_PORT->ODR = pixdata;
RESET_LCD_WR;
SET_LCD_WR;
}
Понятно, что в режиме ввода и в альтернативных функциях возможность использования сохранятся, они не зависят от регистра ODR.
Это даст еще некоторое ускорение, до 246 мс.
Двигаемся дальше.
Следующим этапом вынесем основной цикл перебора пикселов в функцию на уровень глубже и попробуем сделать программный вариант эмуляции работы канала DMA, прямого доступа к памяти. Для этого нам надо перенести линию управления WE дисплея в группу, где расположена шина данных, т.е. GPIO B. Пусть это будет PB9.
void SSD1963_WriteDataPix_9(uint32_t pixdata, uint32_t n){
static uint32_t dp[4];
uint8_t i;
SET_LCD_RDS;
RESET_LCD_WR;
dp[0] = (pixdata >> 9) & 0x01ff;
dp[1] = ((pixdata >> 9) & 0x01ff) | 0x0200;
dp[2] = pixdata & 0x01ff;
dp[3] = (pixdata & 0x01ff) | 0x0200;
for (;n;n--){
for (i=0;i<4;i++) {
LCD_DATA_PORT->ODR = dp[i];
}
}
void SSD1963_ClearScreen_9(uint32_t color)
{
SSD1963_SetArea(0, TFT_WIDTH-1 , 0, TFT_HEIGHT-1);
SSD1963_WriteCommand(0x002c);
SSD1963_WriteDataPix_9(color, TFT_HEIGHT*TFT_WIDTH);
}
Как видно из кода, мы последовательно записываем 9 варианта данных в группу портов B, где кроме 9-битной шины данных расположен также сигнал WE. Операция » | 0×0200» — это как раз выставление этого сигнала. Такой код дает великолепный прирост до 85 мс, а если заменить определение массива «static uint32_t dp[4]» на «static uint16_t dp[4]», то и до 75 мс. Для проверки был замерен вариант с включением режима DMA и такой же передачей содержимого 4-х ячеек в порт ввода-вывода. Результат всего лишь 230 мс. Почему DMA медленнее? Все просто, в программном режиме компилятор оптимизирует код и все 4 значения размещаются в регистрах процессора, а не в памяти, а выборка из памяти, которая выполняется контроллером DMA, идет значительно медленнее, чем работа с регистрами.
Скомпилированный основной цикл выглядит так:
08000265: ldr r3, [pc, #24] ; (0x8000280
08000267: str r6, [r3, #12]
08000269: str r5, [r3, #12]
0800026b: str r4, [r3, #12]
0800026d: str r1, [r3, #12]
0800026f: subs r2, #1
08000271: bne.n 0x8000266
В этом варианте, а также в варианте с каналом DMA остается ограничение на использование портов PB10-PB15. Однако на них можно вывести сигналы дисплея RST и D/C и учесть их в цикле, тогда ограничений будем меньше.
Таким образом, мы достигли максимальной скорости заполнения целиком экрана или прямоугольной области одним цветом. Вроде бы это предел, но можно ввести еще одно ограничение и двинуться чуть дальше.
Дело в том, что в некоторых применениях дисплея не нужен весь набор цветов (в RGB656 — 65536 цветов). Например, в области АСУТП, где требуется отображать состояние производственного объекта, либо какое-то текстовое применение, отображение сообщений. Если это предположение верно, и нам не требуется отображать полноцветные фото- и видеоматериалы, то попробуем продолжить оптимизацию.
Рассмотрим палитру, где у каждого цвета равны первая и вторая часть данных, передаваемых по шине в дисплей. Т.е. из 18 бит модели RGB666, первые 9 бит равны вторым 9. Это дает нам 2^9=512 цветов. Возможно кому-то покажется недостаточно, но для построения графиков или отображения алфавитно-цифровой информации вполне может хватить. Назовем их условно «симметричные цвета».
Вот выборка из них, 100 штук, более наглядно:
Что нам дает использование только этих цветов? Да то что для заполнения области нам не надо менять состояние шины данных в процессе заливки. Достаточно переключать состояние сигнала WE и считать сколько раз мы это сделали. Более того, можно инвертировать WE сколько угодно долго, главное не меньше чем нужно для заполнения области. Нетрудно посчитать, что раз на один пиксел нам требуется передать два блока данных по шине, то требуется 2 подтверждения сигналом WE. Соответственно на весь экран надо (Ширина_экрана*Длина_экрана*2) импульсов, или 800×240*2=768000.
Как проще генерировать импульсы. Конечно! Можно использовать таймер. TIM1 в данном контроллере более быстрый, чем таймеры TIM2-TIM4, т.к. находится на более скоростной шине тактирования APB2. Исследования показали, что включив таймер в режиме ШИМ генератора с минимальным делителем можно получить время заполнения 32 мс! Понятно что сигнал WE надо снимать с выхода таймера, например PA8 (TIM1_CH1).
Можно еще увеличить скорость заполнения? Оказалось да, просто подав сигнал SYSCLK с выхода RCC_MCO на вхож WE LCD. Это максимальная доступная частота на процессоре, 72 МГц. Время заполнения дисплея симметричным цветом составляет 10.7 мс.
Отсчитывается время таймером, после чего по прерыванию сигнал снимается, а порт переключается в режим вывода.
//инициализация таймера
void SSD1963_TimInit2(void){
TIM_TimeBaseInitTypeDef Timer;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
TIM_TimeBaseStructInit(&Timer);
Timer.TIM_Prescaler = 72-1;
Timer.TIM_Period = 10000;
Timer.TIM_CounterMode = TIM_CounterMode_Down;
TIM_TimeBaseInit(TIM4, &Timer);
TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE);
NVIC_EnableIRQ(TIM4_IRQn);
}
void SSD1963_WriteDataPix(uint32_t pixdata, uint32_t n){
GPIO_InitTypeDef GPIO_InitStr;
SET_LCD_RDS;
LCD_DATA_PORT->ODR = (LCD_DATA_PORT->ODR & ~0x01ff) | (pixdata & 0x01ff);
GPIO_InitStr.GPIO_Pin = LCD_WR;
GPIO_InitStr.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStr.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(LCD_WR_PORT, &GPIO_InitStr); // включение альтернативной функции
TIM_ITConfig(TIM4, TIM_IT_Update, DISABLE); // запрет вызова прерывания во время обновления делителя
if (n > 32000 ){
TIM_PrescalerConfig(TIM4, 72 - 1, TIM_PSCReloadMode_Immediate); // период 1 мкс
TIM4->CNT = (uint16_t) (n / 36); // вычисляем время в мкс на заливку
} else {
TIM_PrescalerConfig(TIM4, 0, TIM_PSCReloadMode_Immediate); // период 1/72 мкс (минимальный)
TIM4->CNT = (uint16_t) (n * 2 - 1); // два такта на заливку пикселя
}
TIM_ClearITPendingBit(TIM4, TIM_IT_Update); // сброс запроса прерывания
TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE); // разрешение прерывания
RCC_MCOConfig(RCC_MCO_SYSCLK); //MCO выберем источник
TIM4->CR1 |= TIM_CR1_CEN; //запуск таймера
}
void TIM4_IRQHandler()
{
GPIO_InitTypeDef GPIO_InitStr;
if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM4, TIM_IT_Update); // сброс запроса прерывания
TIM_Cmd(TIM4, DISABLE); // выключение таймера
RCC_MCOConfig(RCC_MCO_NoClock); // выключение SYSCLK на выходе MCO
GPIO_InitStr.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStr.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStr.GPIO_Pin = LCD_WR;
GPIO_Init(LCD_WR_PORT, &GPIO_InitStr); // переключение порта в режим вывода
}
}
void SSD1963_ClearScreen_9(uint32_t color)
{
SSD1963_SetArea(0, TFT_WIDTH-1 , 0, TFT_HEIGHT-1);
SSD1963_WriteCommand(0x2c);
SSD1963_WriteDataPix(color, TFT_HEIGHT*TFT_WIDTH);
}
int main(void){
tick_init(); // инициализация системного таймера
lcd_port_init(); // инициализация портов ввода-вывода
SSD1963_Init(); // инициализация дисплея
SSD1963_TimInit2(); // инициализация таймера TIM4
SSD1963_ClearScreen_9(0x1ff); // Заливка экрана белым цветом
while(1) {}
}
Таймер отсчитывает время с точностью 1/72 мкс для количества точек, меньшего 32000 и с точностью 1 мкс для большего количества точек. Это связано с разрядностью счетчика таймера. Учитывая, что требуется какое-то время на обработку прерывания при выключении таймера, сигнал на выходе MCO снимается немного позднее, чем требуется, с небольшим запасом. Экспериментально установлено что это около 10–11 тактов частоты процессора. Таким образом, можно сказать что есть порог использования данной методики, при котором она остается быстрее, несмотря на накладные расходы на инициализацию таймера и RCC_MCO и отключение. Квадрат 2×2 пиксела вероятно выгоднее программно заполнять по циклу.
В качестве вывода можно сказать что, добавив некоторые ограничения, было уменьшено время заполнения экрана с 375 до 11 мс. Кроме того, заполнение идет без участия процессора, который в это время может выполнять другие задачи.
Буду рад замечаниям и дополнениям.