STM32 и LCD, быстрая заливка экрана

В настоящее время получили распространение различные жидкокристаллические дисплеи, которые отлично подключаются к контроллерам семейства STM32. В данной статье речь пойдет об одном из распространенных контроллеров STM32F103C8T6 и дисплее 7" на контроллере SSD1963. Оба в виде законченных узлов легко доступны на Aliexpress и относительно недорого стоят. Конечно, все рассмотренное ниже справедливо и для других дисплеев с параллельным интерфейсом и большинства контроллеров STM32.

Вот так выглядят соединяемые устройства:

image

image

В комплекте с дисплеем была распиновка выводов и код инициализации для 51-го контроллера с комментариями на языке изготовителя.

Коротко о подключении


Подключение дисплея заключается в подаче питания 3.3 и 5 вольт на нужные выводы и соединении информационных линий с контроллером. Управляющие сигналы D/C, WE, RST подключатся к свободным линиям ввода-вывода на процессоре. В нашем случае это D/C — PA1, WE — PA8, RST — PA2. Сигналы RD и CS можно не использовать, при этом на RD надо подать логическую единицу, т.е. подключить через резистор (в данном случае 4,7 КОм) на +3.3 В, а на CS — «0», т.е. подключить на землю.

Замечание
Дисплей сконфигурирован производителем для работы интерфейса в режиме 8080, и, согласно документации, сигнал CS «выборка кристалла» должен быть задействован:

image

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

Далее нужно подключить шину данных. У данного дисплея она предполагается 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<TFT_WIDTH;x++){
    for(y= 0;y<TFT_HEIGHT;y++){
      SSD1963_WriteDataPix(color);
    }
  }
}


Как видно, код зависит от выбранной разрядности шины. Соответственно зависит и время, необходимое для выполнения передачи пикселя на дисплей. Для 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<TFT_WIDTH;x++) {
    for(y= 0;y<TFT_HEIGHT;y++) {
      SSD1963_WriteDataPix_9(color);
    }
  }
}

Изменили кодировку цвета вместо 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);
}

Как видно из кода, мы последовательно записываем 4 варианта данных в группу портов B, где кроме 9-битной шины данных расположен также сигнал WE. Операция " | 0x0200" — это как раз выставление этого сигнала. Такой код дает великолепный прирост до 85 мс, а если заменить определение массива «static uint32_t dp[4]» на «static uint16_t dp[4]», то и до 75 мс. Для проверки был замерен вариант с включением режима DMA и такой же передачей содержимого 4-х ячеек в порт ввода-вывода. Результат всего лишь 230 мс. Почему DMA медленнее? Все просто, в программном режиме компилятор оптимизирует код и все 4 значения размещаются в регистрах процессора, а не в памяти, а выборка из памяти, которая выполняется контроллером DMA, идет значительно медленнее, чем работа с регистрами.
Скомпилированный основной цикл выглядит так:

08000265: ldr r3, [pc, #24] ; (0x8000280 <SSD1963_WriteDataPix_9+84>)
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 <SSD1963_WriteDataPix_9+58>

В этом варианте, а также в варианте с каналом DMA остается ограничение на использование портов PB10-PB15. Однако на них можно вывести сигналы дисплея RST и D/C и учесть их в цикле, тогда ограничений будем меньше.

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

Дело в том, что в некоторых применениях дисплея не нужен весь набор цветов (в RGB656 — 65536 цветов). Например, в области АСУТП, где требуется отображать состояние производственного объекта, либо какое-то текстовое применение, отображение сообщений. Если это предположение верно, и нам не требуется отображать полноцветные фото- и видеоматериалы, то попробуем продолжить оптимизацию.
Рассмотрим палитру, где у каждого цвета равны первая и вторая часть данных, передаваемых по шине в дисплей. Т.е. из 18 бит модели RGB666, первые 9 бит равны вторым 9. Это дает нам 2^9=512 цветов. Возможно кому-то покажется недостаточно, но для построения графиков или отображения алфавитно-цифровой информации вполне может хватить. Назовем их условно «симметричные цвета».

Симметричные цвета
Вот они выведены на экран:



Вот выборка из них, 100 штук, более наглядно:



Что нам дает использование только этих цветов? Да то что для заполнения области нам не надо менять состояние шины данных в процессе заливки. Достаточно переключать состояние сигнала WE и считать сколько раз мы это сделали. Более того, можно инвертировать WE сколько угодно долго, главное не меньше чем нужно для заполнения области. Нетрудно посчитать, что раз на один пиксел нам требуется передать два блока данных по шине, то требуется 2 подтверждения сигналом WE. Соответственно на весь экран надо (Ширина_экрана*Длина_экрана*2) импульсов, или 800*480*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 мс. Кроме того, заполнение идет без участия процессора, который в это время может выполнять другие задачи.

Буду рад замечаниям и дополнениям.
Share post

Similar posts

Comments 21

    0
    Некоторое время назад подключил к значительно более медленному кристаллу более маленький LCD, имея разведённой на стороне LCD лишь 8-ми битную шину.
    В итоге родилась маленькая конструкция-монстрик, которая работает аналогично Вашему примеру в конце, но… в общем, смотрите сами:

    Пара функций
    <p>void LCD_FillRect(short left, short top, short right, short bottom, int Color) {
    int iterations = 0;
    char R,G,B;
    int Max;
    LCD_SetBounds(left, top, right, bottom);
    Max = (right+1-left) <em> (bottom+1-top);
    R = Color & 0xFF;
    G = Color >> 8 & 0xFF;
    B = Color >> 16 & 0xFF;
    if ((R == G) & (G == B)) {
    WriteOneByteManyTimes(R, Max</em>3);
    } else {
    while (iterations < Max) {
    WriteData(B);
    WriteData(G);
    WriteData(R);
    iterations = iterations + 1;
    };
    };
    };
    void WriteOneByteManyTimes(uint8_t byte, int iterations) {
    int i;
    GPIOC->BSRR = (GPIO_BSRR_BR_9 | GPIO_BSRR_BS_7);
    GPIOA->ODR = (byte);
    for (i=0;i<iterations;i++){
    GPIOC->BSRR = GPIO_BSRR_BR_8;
    GPIOC->BSRR = GPIO_BSRR_BS_8;
    };
    GPIOC->BSRR = (GPIO_BSRR_BS_9| GPIO_BSRR_BS_8 | GPIO_BSRR_BS_7 | GPIO_BSRR_BS_6);
    };
    



    PS: Что за ужас стал с тэгом "source"?
      0
      Исправил:

      Пара функций
      void LCD_FillRect(short left, short top, short right, short bottom, int Color) {
          int iterations = 0;
          char R,G,B;
          int Max;
          LCD_SetBounds(left, top, right, bottom);
          Max = (right+1-left) * (bottom+1-top);
          R = Color & 0xFF;
          G = Color >> 8 & 0xFF;
          B = Color >> 16 & 0xFF;
          if ((R == G) & (G == B)) {
              WriteOneByteManyTimes(R, Max*3);
          } else {
              while (iterations < Max) {
                  WriteData(B);
                  WriteData(G);
                  WriteData(R);
                  iterations = iterations + 1;
              };
          };
      };
      void WriteOneByteManyTimes(uint8_t byte, int iterations) {
          int i;
          GPIOC->BSRR = (GPIO_BSRR_BR_9 | GPIO_BSRR_BS_7);
          GPIOA->ODR = (byte);
          for (i=0;i<iterations;i++){
              GPIOC->BSRR = GPIO_BSRR_BR_8;
              GPIOC->BSRR = GPIO_BSRR_BS_8;
          };
          GPIOC->BSRR = (GPIO_BSRR_BS_9| GPIO_BSRR_BS_8 | GPIO_BSRR_BS_7 | GPIO_BSRR_BS_6);
      };

        0
        как у нее со скоростью в итоге?
          0
          Грубоватая оценка будет, т.к. не замерял, но в целом менее 0.1 секунды (на глазок). Кристалл — STM32L152RBT6, тактирование, если мне не изменяет память, 16МГц. Дома попробую уточнить скорость заливки.
          Проверял отличия между "обычным" методом (глаз видит, порядка 0.3 секунд заливается чёрным), и "быстрым", где только дёргается пин #WR. Разница отменная.
          К сожалению, в моём случае, можно залить только чёрным/серым/белым.
            0
            Итак. Сделал замер скорости.
            Сразу оговорюсь — лично мне приходится использовать 8-ми битную шину, а цвет для простоты передаю в 18 битном формате (3 байта / пиксел).
            Для простой функции скорость получилась:
            При 16MHz ядра:

            • Обычная (Байт, такт, байт, такт, байт, такт) — 0.3782s
            • Ускоренная (Байт, такт, такт, такт… много-много раз) — 0.1005s
            • Альтернативный вариант (Изменил цикл) — 0.00526s

            Впечатляет. Но полный кадр, полноцветно, заливается долго.
            Варианты кода:

            Обычный
            // Выводим каждый байт, симулируя работу шины 8080
                WriteData(B);
                WriteData(G);
                WriteData(R);


            Ускоренный
            <...>
            Max = (right+1-left) * (bottom+1-top);
            WriteOneByteManyTimes(R, Max*3);
            <...>
            
            void WriteOneByteManyTimes(uint8_t byte, int iterations) {
                int i;
                GPIOC->BSRR = (GPIO_BSRR_BR_9 | GPIO_BSRR_BS_7);
                GPIOA->BSRR = (GPIO_BSRR_BR_0 | GPIO_BSRR_BR_1 | GPIO_BSRR_BR_2 | GPIO_BSRR_BR_3 | GPIO_BSRR_BR_4 | GPIO_BSRR_BR_5 | GPIO_BSRR_BR_6 | GPIO_BSRR_BR_7);
                GPIOA->BSRR = byte;
                for (i=0;i<iterations;i++){
                    GPIOC->BSRR = GPIO_BSRR_BR_8;
                    GPIOC->BSRR = GPIO_BSRR_BS_8;
                };
                GPIOC->BSRR = (GPIO_BSRR_BS_9| GPIO_BSRR_BS_8 | GPIO_BSRR_BS_7 | GPIO_BSRR_BS_6);
            };


            Альтернативный
            <...>
            Max = (right+1-left) * (bottom+1-top);
            WriteOneByteManyTimes(R, Max);
            <...>
                for (i=0;i<iterations;i++){
                    GPIOC->BSRR = GPIO_BSRR_BR_8;
                    GPIOC->BSRR = GPIO_BSRR_BS_8;
                    GPIOC->BSRR = GPIO_BSRR_BR_8;
                    GPIOC->BSRR = GPIO_BSRR_BS_8;
                    GPIOC->BSRR = GPIO_BSRR_BR_8;
                    GPIOC->BSRR = GPIO_BSRR_BS_8;
                };


            В последнем варианте — пин дёргается трижды за цикл и конвеер приходится перезагружать реже — в итоге получаем ускорение работы подпрограммы за счёт уменьшения количества инструкций перехода.

            В сухом остатке:
            Изначальная скорость заполнения экрана (320*240) px при глубине цвета в 24 бита (фактически, 18, т.к. два последних бита игнорируются) на тактовой в 16MHz (внутренний высокочастотный генератор) — те же 378.2ms (результат чуть хуже, чем у автора).
            При заполнении серым цветом, результат уже лучше — можно добиться 52.6ms, без использования аппаратных средств, на голом "ногодрыге" одним пином.
            Не самый лучший результат, согласен. Но на другом кристалле (STM32F107V) на частоте 50MHz обновление экрана вообще едва заметно глазу. Особенно, заливка чёрным в самом начале (грубо могу оценить в 32ms без оптимизации цикла и в 17 при оптимизации).

            ЗЫ: Придираться к форматированию кода в блоке не буду, подбирать параметры — тоже.
              0
              Хороший результат. Точек конечно намного меньше. Меня на подобном экране (2.2") процессор STM32F030 на частоте 12 МГц вполне устраивал.
        0
        Да, близко, но вы формируете импульсы записи программно, а не аппаратно, и в случае раздельных трех байт, каждый со своим цветом, быстрая запись у вас возможна только белого, черного и серых цветов.
          0
          В случае ТС, можно залить любым из 16 битного цвета, т.к. фактически он дёргает пином записи и передаёт за такт те самые 16 бит.
            0
            Согласен, однако у данного микроконтроллера не много портов. Занимая все порты группы GPIOB, терялась возможность использовать шину I2C, она разведена только в группе B. При использование же 9 бит, она доступна. Таким образом получается универсальное решение для дальнейшей разработки и использования SPI, I2C, UART.

            Как-то так

          +1
          Выводить на экран через порты, можно лишь на платах, которые криво разведены (спроектированы).

          У STM32F103 есть же FSMC контроллер!

          В этом режиме работа с экраном идет, мягко говоря, значительно быстрее, чем через порты..

          "Очистка" экрана при этом выглядит как то вот так:

          #define Black           0x0000      /*   0,   0,   0 */
          
          #define Bank1_LCD_D    ((uint32_t)0x60020000)    //disp Data ADDR
          #define Bank1_LCD_C    ((uint32_t)0x60000000)    //disp Reg ADDR
          
          void clearScr(uint16_t color)  {
           *(__IO uint16_t *) (Bank1_LCD_C)= 0x50;
           *(__IO uint16_t *) (Bank1_LCD_D)= 0;
           *(__IO uint16_t *) (Bank1_LCD_C)= 0x51;
           *(__IO uint16_t *) (Bank1_LCD_D)= 239;
           *(__IO uint16_t *) (Bank1_LCD_C)= 0x52;
           *(__IO uint16_t *) (Bank1_LCD_D)= 0;
           *(__IO uint16_t *) (Bank1_LCD_C)= 0x53;
           *(__IO uint16_t *) (Bank1_LCD_D)= 319;
           *(__IO uint16_t *) (Bank1_LCD_C)= 32;
           *(__IO uint16_t *) (Bank1_LCD_D)= 0;
           *(__IO uint16_t *) (Bank1_LCD_C)= 33;
           *(__IO uint16_t *) (Bank1_LCD_D)= 0;
           *(__IO uint16_t *) (Bank1_LCD_C)= 34;
           for(i=0;i<76800;i++)  *(__IO uint16_t *) (Bank1_LCD_D)= color;
          }

          Время обновления экрана не замерял, но на глаз — мгновенно.
          Нормальный экран с динамически отображаемым текстом и графикой можно сделать только с FSMC.
          Попытка использовать плату c управлением LCD через порты ничего кроме раздражения заметными на глаз задержками не вызывает.
            +2
            Судя по данным STMicroelectronics, FSMC есть только у серии STM32F4 и STM32L162QD. В данном случае планировалось бюджетное решение, а STM32F103 и STM32F4 находятся в разных ценовых категориях.
              +1
              В серии STM32F103 есть контроллеры с FSMC, но все они большие — от 100 выводов, например STM32F103VET6.
              upd: тоже не успел.
                0
                Да, тоже уже посмотрел. Странно что в сводной таблице на сайте STM эта информация отсутствует.
              +1
              21 Flexible static memory controller (FSMC)
              Low-density devices are STM32F101xx, STM32F102xx and STM32F103xx
              microcontrollers where the Flash memory density ranges between 16 and 32 Kbytes.
              Medium-density devices are STM32F101xx and STM32F103xx microcontrollers where
              the Flash memory density ranges between 32 and 128 Kbytes.
              High-density devices are STM32F101xx and STM32F103xx microcontrollers where the
              Flash memory density ranges between 256 and 512 Kbytes.
              XL-density devices are STM32F101xx and STM32F103xx microcontrollers where the
              Flash memory density ranges between 768 Kbytes and 1 Mbyte.
              Connectivity line devices are STM32F105xx and STM32F107xx microcontrollers.
              This section applies to high-density and XL-density devices only.

              Но описанный автором контроллер — F103C8T6, НЕ относится к "high-density and XL-density devices", так что там НЕТ FSMC контроллера. Потому — велосипеды и костыли.

              upd: Не успел.
                0
                Да, верно, FSMC в 103-й серии появляется у STM32F103RC, первого чипа в этой серии с 256М флеша.
                Вот из его Key Features:

                Memories

                • 256 to 512 Kbytes of Flash memory
                • up to 64 Kbytes of SRAM
                • Flexible static memory controller with 4 Chip Select. Supports Compact Flash, SRAM, PSRAM, NOR and NAND memories
                • LCD parallel interface, 8080/6800 modes
                  0
                  Сорри, ошибся. В документации есть примечание "For devices delivered in LQFP64 packages, the FSMC function is not available."
                  Так что FSMC начинается с STM32F103VC, у него 100-ногий корпус.
                0
                Опс… не обратил внимания на фотку платы контроллера и его тип (что не High-density).

                Видимо подсознание отбросило этот факт… поскольку, лично я бы, к такому не стал бы пытаться LCD дисплей подключить. Особенно, если требуется от него (экран) вывод динамических данных.

                в конце концов, STM32F103ZET6 core board не сильно дороже стоит. Для "поиграться", как минимум.
                  0
                  Ну смотря сколько не жалко потратить.
                  Плата с контроллером из данной статьи стоит сейчас около 160 р.
                  Минимальная с FSMC (STM32F103VCT6) уже от 830. А плата с STM32F103ZET6 от 1000 р.
                    0
                    Ну если для кого то 700р это большие деньги… тогда опс..
                    0
                    Эх. Я бы тоже поставил дисплей на контроллер, но… в малых корпусах просто нет его. Так что, костыли и велосипеды — вот наш девиз XD
                      0
                      Согласен. Да и это наверное не совсем костыли. Это скорее нестандартное использование, выявление скрытых возможностей.
                      Костыли — это когда например что-то глючит, и наугад ставим задержки чтобы "устаканились" переходные процессы. Или вводим какую-нибудь глобальную переменную, в одном месте в нее заносим что-то, а далеко в другом — проверяем.
                      А у нас просто оптимизация в рамках заданных аппаратных средств.

                Only users with full accounts can post comments. Log in, please.