Video на stm32duino: ov7670 + stm32f103c8t6 + ili9341

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015
Скажу честно: зачем мне может понадобиться подобная конструкция, я не знаю. Сам на форуме несколько раз заявлял, что Ардуино и видео - вещи несовместимые. Но вот соблазнился на низкую цену и заказал себе камеру (вдруг когда-нибудь пригодится), а когда получил, обнаружил, что проверить работоспособность не так просто.
В общем, проект не является утилитарным, а подразумевает исключительно спортивный интерес.
Итак, что имеем:
- самую дешевую камеру (что-то в районе $2),
- самый дешевый дисплей 320х240 - увы, только в виде шилда для Uno (все другие варианты - дороже).
- AVR вряд ли подходят по производительности, на Due неудобно (и не полностью) разведены порты, Due Core - жаба душит, а BluePill - самый раз по всем позициям, особенно, учитывая, что это самый дешевый 32-разрядный контроллер.
Итак, получилась конструкция из трех самых дешевых компонент.
Кстати, 3 штучки BluePill в свое время заказал, но так и не удосужился их даже попробовать. Теперь, наконец, руки дошли. Один, кстати, оказался неисправным :(
По логике, можно было бы просто соединить выход камеры с входом дисплея, предварительно настроив их так, чтобы они понимали сигнал друг друга. А контроллер задействовать только в шине управления. Тогда и Pro Mini за глаза хватило бы. Но как же спортивный интерес? Опять же, если мы говорим об обработке, то процесс выглядит так: чтение_с_камеры->обработка_в_контроллере->вывод_на_дисплей. Так что, чтобы иметь возможность как-то включить обработку, нужно по меньшей мере иметь первую и последнюю стадии. Вот этим, собственно, и займемся.
Ко всему оказалось, что BluePill еще и оптимален по количеству потов ввода-вывода:
- для камеры нужно: 8 - шина данных, 2 - шина SCCB, 4-5 - шина управления,
- для дисплея нужно: 8 - шина данных, 4-5 - шина управления,
итого: 26-28 контактов.
У BluePill всего 35 контактов, некоторые из которых, правда, 8 используются по другому назначению:
- PB2 - используется в качестве BOOT1 - на гребенку не разведен,
- PA13-PA14 - используются ST-Link - на гребенку не разведены,
- PA11-PA12 - используются USB - на гребенке присутствуют,
- PC13 - используется для светодиода-индикатора - на гребенке присутствует,
- PC14-PC15 - соединены с кварцем на 32768 для RTC - на гребенке присутствуют.
т.е. получается, что всего в обрез.
По факту, решил отказаться от RESET на дисплее, благо для этого есть команда. Хотя, вероятно, лучше было бы отказаться от сигнала READ дисплея, т.к. он включен через преобразователь уровня, а подавать на 3-вольтовый контроллер 5 Вольт сразу по 8 каналам - чревато.
При проектировании распиновки выяснилась одна неприятная особенность: если мы хотим передавать данные с камеры на дисплей без дополнительных манипуляций, номера пинов шины данных порта А должны соответствовать номерами пинов шины данных порта В, причем, в порте В занят один пинов из младшей половины, а в порте А - несколько пинов из старшей. Поэтому шину данных пришлось расположить на пинах с 3 по 10.
Вот что получилось в результате:
Для отладки на схеме предусмотрел несколько точек подключения логического анализатора или осциллографа (показаны кружками), а также немного разделил выход данных SCCB контроллера и камеры - опять же для облегчения отладки.
Посмотрев на дохленький стабилизатор питания BluePill не рискнул нагружать его еще и камерой, поэтому предусмотрел внешний стабилизатор.
 
b707
Offline
Зарегистрирован: 26.05.2017

andriano. жду продолжения. DMA будет?

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Буду пробовать несколько вариантов. Среди них, возможно, будет и DMA. С stm32 работаю меньше двух недель, но пощупать DMA желание есть. Пока есть только опыт программирования DMA на IBM PC под DOS.

dimax
dimax аватар
Offline
Зарегистрирован: 25.12.2013

Мне кажется DMA тут не светит, c обычными портами оно не умеет общаться.

ssss
Offline
Зарегистрирован: 01.07.2016

С какого это перепуга??? Не факт что будет быстрее с ДМА... из-за латентности... но работать будет...

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015
Начал, естественно, с того, что посмотрел, а что подобное уже кем-то когда-то создавалось.
Результат не очень обнадеживающий:
в большинстве случаев данные передаются на ПК, естественно, с довольно низкой скоростью, так что говорить о видео в реальном времени не приходится, скорее - фото, да и то смазанное из-за того, строки берутся со значительной сдвижкой во времени.
Причем, в большинстве случаев авторы довольствуются черно-белым изображением.
Там же, где изображение и выводилось на собственный дисплей, а не в ПК, да еще и в цвете, было замечено два момента:
- использование stm32f4, что, на мой взгляд, несколько мощнее stm32f1,
- дисплей 1.8" с максимальным разрешением 160х128, причем, заметно, что для изображения используется не вся площадь экрана.
 
Еще один момент, на который наткнулся, читая статьи по теме: для управления камерой используется протокол SCCB, близкий к I2C. Но несколько разных авторов (которые, в основном, работали на Due или STM32) указывают, что при подключении к аппаратному порту I2C у них возникают проблемы.
Поэтому решил сразу делать софтверный SCCB строго по дэйташиту, чтобы потом не чесать репу, а где проблема: у меня в коде, в коде библиотеки или имеет место аппаратная несовместимость.
Обнаружились две вещи:
1. Оказалось, что пока не подашь на камеру внешнюю частоту, ни на какой SCCB она отвечать и не собирается.
2. Код отладки не потребовал, так что зря я добавлял на плату различные ухищрения для, как мне казалось, облегчения отладки.
 
Собственно, код ниже:
SCCB.h
#ifndef SCCB_H
#define SCCB_H

#include <Arduino.h>

#define SCCB_SDA 0x0001
#define SCCB_SCL 0x0002

class cSCCB {
public:
  void pause();
  uint8_t readRegister(uint8_t r);
  void writeRegister(uint8_t r, uint8_t d);
private:
  void start();
  void writeBit(bool b);
  void writeByte(uint8_t d);
  uint8_t readBit();
  uint8_t readByte();
  void stop();
};

#endif

 

 
SCCB.cpp
#include "sccb.h"

void cSCCB::start() {
  delayMicroseconds(4);
  GPIOB_BASE->BRR = SCCB_SDA;
  delayMicroseconds(2);
  GPIOB_BASE->BRR = SCCB_SCL;
  delayMicroseconds(2);
}

void cSCCB::writeBit(bool b){
  if(b)
    GPIOB_BASE->BSRR = SCCB_SDA;
  else
    GPIOB_BASE->BRR = SCCB_SDA;
  delayMicroseconds(2);
  GPIOB_BASE->BSRR = SCCB_SCL;
  delayMicroseconds(4);
  GPIOB_BASE->BRR = SCCB_SCL;
  delayMicroseconds(2);
}

void cSCCB::writeByte(byte d){
  for(int i = 0; i < 8; i++) {
    writeBit(d & 0x80);
    d <<= 1;
  }
  writeBit(1); // ASK
}

byte cSCCB::readBit(){
  delayMicroseconds(2);
  GPIOB_BASE->BSRR = SCCB_SCL;
  delayMicroseconds(2);
  byte b = (byte)((GPIOB_BASE->IDR & SCCB_SDA) != 0);
  delayMicroseconds(2);
  GPIOB_BASE->BRR = SCCB_SCL;
  delayMicroseconds(2);
  return b;
}

byte cSCCB::readByte(){
  byte d = readBit();
  for(int i = 1; i < 8; i++) {
    d = (d << 1) + readBit();
  }
  writeBit(1); // ASK
  return d;
}

void cSCCB::stop(){
  GPIOB_BASE->BRR = SCCB_SDA;
  delayMicroseconds(2);
  GPIOB_BASE->BSRR = SCCB_SCL;
  delayMicroseconds(6);
  GPIOB_BASE->BSRR = SCCB_SDA;
}

void cSCCB::pause(){
  delayMicroseconds(8);
}

byte cSCCB::readRegister(byte r) {
  pause();
  start();
  writeByte(0x42);
  writeByte(r);
  stop();  

  pause();
  start();
  writeByte(0x43);
  byte d = readByte();
  stop();  
  pause();
  return d;
}

void cSCCB::writeRegister(byte r, byte d) {
  pause();
  start();
  writeByte(0x42);
  writeByte(r);
  writeByte(d);
  stop();  
  pause();
}

 

 
На Ардуино, конечно, очень модно собирать все на беспаячных макетках и проводках Dupont. Это один вариант. Другой - сразу ЛУТ. Честно говоря, предпочитаю что-то посередине, а именно: обычные макетки для пайки.
Здесь, правда, Ардуино тоже подкузьмила: ну кто, спрашивается, просил располагать одну из гребенок со сдвижкой на половину шага? А, как мы помним, самый дешевый дисплей 320х240 имеет как раз посадочное место точно под Uno. 
В общем, взял заготовку макетки для Меги и на ней собрал всю коммутацию. Благо, десяток пришел совсем недавно, о чем я писал в теме "Интересное на Али-экспресс". Только вот оказалось, что хотя плата и двухсторонняя, дырочки у нее не металлизированные. 
Естественно, от дырок для стандартных разъемов идут дорожки к соседним дырочкам, но воспользоваться этим можно только при изготовлении стандартного шилда, т.е. когда контакты торчат вниз, а распаиваются сверху (именно сверху и проходят все дорожки). У меня же другой случай: мне нужно вставить дисплей, в этом случае макетка должна для дисплея имитировать посадочное место Uno, т.е. гнезда должны торчать вверх, а подпаиваться снизу. Но контактные площадки снизу не имеют контакта с верхними площадками, соответственно дорожки с верхней стороны платы абсолютно бесполезны. Пришлось подпаивать МГТФ прямо к ножкам контактных гнезд.
Ну а камера с ее двухрядным разъемом прекрасно разместилась на куске двойного разъема Меги. Естественно, и BluePill, и камера расположились с обратной стороны от дисплея.
Правда, отсутствие металлизации и здесь попортило нервы: оказалось, что капля припоя не хочет как следует ложиться на контактную площадку, охватывая пин разъема. Пришлось делать из провода 0.2мм колечки, чтобы капля припоя легла, как надо.
 
На этой стороне отчетливо видна имитация Uno - чтобы дисплей не заметил подмены.
 
А это обратная сторона: слева-снизу - разъем для камеры, сверху-справа - для BluePill, слева по верхней и нижней кромкам - разъемы для отладки, а справа по нижней кромки - для 5 В.
 
mixail844
Offline
Зарегистрирован: 30.04.2012

ssss пишет:

С какого это перепуга??? Не факт что будет быстрее с ДМА... из-за латентности... но работать будет...

мб из за того что DMA не умеет работать с GPIO 

мб если настроить

для камеры : 

DMA_CHx_SrcAddr = &GPIOA (у GPIO регистра есть адресс)

DMA_CHy_DstAddr = buffer;

для экрана :

DMA_CHz_SrcAddr = buffer

DMA_CHw_DstAddr = &GPIOB

dimax
dimax аватар
Offline
Зарегистрирован: 25.12.2013

mixail844 пишет:

мб из за того что DMA не умеет работать с GPIO 

Похоже, что действительно можно работать с GPIO, но не как с переферией, а как с адресным пространством. Хорошо бы кто нибудь разобрался и выложил примерчик для stmduino :-))

b707
Offline
Зарегистрирован: 26.05.2017

dimax пишет:

Похоже, что действительно можно работать с GPIO, но не как с переферией, а как с адресным пространством.

Да, на STM32 вся периферия смаплена в области памяти и к любому GPIO можно обращаться, просто читая или записывая данные по определенному адресу.

К сожалению, на практике пока не пробовал :(

ssss
Offline
Зарегистрирован: 01.07.2016

mixail844 пишет:

мб из за того что DMA не умеет работать с GPIO 

Это DMA не умеет работать с GPIO... или вы???

Умеет работать... и точка...

ssss
Offline
Зарегистрирован: 01.07.2016

b707 пишет:

К сожалению, на практике пока не пробовал :(

Хорошая вещь... кстати...

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Первым делом после пайки платы подключил к ней дисплей.

Вывод на дисплей - одна из важных частей проекта, нужно було его как-то пощупать и пооптимизировать.

Вот что получилось:

// для ILI9341 на STM32F102C8T6 72 MHz (оптимизация -O1)
// скорость заполнения (1 пиксель на итерацию)  : 5.52 Мпикс/с (FPS= 71.9)
// скорость заполнения (8 пикселей на итерацию) : 8.32 Мпикс/с (FPS=108.3)
// скорость заполнения (64 пикселя на итерацию) : 8.88 Мпикс/с (FPS=115.6)

#define LCD_RD              0x8000 // PB15   - не используется (если использовать, нужно согласовать уровни сигналов)
#define LCD_WR              0x4000 // PB14   
#define LCD_RS              0x2000 // PB13   
#define LCD_CS              0x1000 // PB12          
#define LCD_RESET           0x0800 // PB11   
#define DATA_MASK           0x07f8 // PB3-PB10 (8, 9, 2-7)
#define CTRL_MASK_DEFAULT   0xf803 // бит PB2 не используется, поэтому не 0xf107: все управляющие биты HIGH
#define CTRL_MASK_WR_RS_SC  0x8803 // маска для записи команд
#define CTRL_MASK_WR_SC     0xa803 // маска для записи данных

// pin     PB15  PB14  PB13  PB12  PB11  PB10  PB9  PB8  PB7  PB6  PB5  PB4  PB3  PB2  PB1  PB0
// func     RD    WR    RS    CS   REST   D7    D6   D5   D4   D3   D2   D1   D0   -   SDA  SCL
// default  1     1     1     1     1     x     x    x    x    x    x    x    x    -   1    1 : ODR
// mode     3     3     3     3     3     3     3    3    3    3    3    3    3    4   7    7 : CRH/CRL

void Lcd_Write_Com(unsigned char d)  // вывод команды: опустить PB13, вывести байт, поднять PB13
{   
  GPIOB_BASE->ODR = CTRL_MASK_WR_RS_SC | (d << 3);
  GPIOB_BASE->BSRR = LCD_WR;
}

inline void Lcd_Write_Data(unsigned char d)
{
  GPIOB_BASE->ODR = CTRL_MASK_WR_SC | (d << 3);
  GPIOB_BASE->BSRR = LCD_WR;
}

void Address_set(unsigned int x1,unsigned int y1,unsigned int x2,unsigned int y2) // позиционирование на экране (оптимизировано)
{
  GPIOB_BASE->ODR = 0x8953; // команда 0x2a - Column Address Set
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = CTRL_MASK_WR_SC; // считая, что x1 <= 240, старший байт равен 0
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = CTRL_MASK_WR_SC | ((x1 & 0xff) << 3); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = CTRL_MASK_WR_SC; // считая, что x2 <= 240, старший байт равен 0
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = CTRL_MASK_WR_SC | ((x2 & 0xff) << 3); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = 0x895b; // команда 0x2b - Page Address Set
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = CTRL_MASK_WR_SC | ((y1 & 0xff00)>>5); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = CTRL_MASK_WR_SC | ((y1 & 0xff) << 3); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = CTRL_MASK_WR_SC | ((y2 & 0xff00)>>5); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = CTRL_MASK_WR_SC | ((y2 & 0xff) << 3); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = 0x8963; // команда 0x2c - Mempry Write
  GPIOB_BASE->BSRR = LCD_WR;
}

void Lcd_Init(void) // вызываем один раз, поэтому по скорости не оптимизируем
{
  GPIOB_BASE->BSRR = LCD_RESET;
  delay(5); 
  GPIOB_BASE->BRR = LCD_RESET;
  delay(15);
  GPIOB_BASE->BSRR = LCD_RESET;
  delay(15);

  Lcd_Write_Com(0xCB);  
  Lcd_Write_Data(0x39); 
  Lcd_Write_Data(0x2C); 
  Lcd_Write_Data(0x00); 
  Lcd_Write_Data(0x34); 
  Lcd_Write_Data(0x02); 

  Lcd_Write_Com(0xCF);  
  Lcd_Write_Data(0x00); 
  Lcd_Write_Data(0XC1); 
  Lcd_Write_Data(0X30); 

  Lcd_Write_Com(0xE8);  
  Lcd_Write_Data(0x85); 
  Lcd_Write_Data(0x00); 
  Lcd_Write_Data(0x78); 

  Lcd_Write_Com(0xEA);  
  Lcd_Write_Data(0x00); 
  Lcd_Write_Data(0x00); 
 
  Lcd_Write_Com(0xED);  
  Lcd_Write_Data(0x64); 
  Lcd_Write_Data(0x03); 
  Lcd_Write_Data(0X12); 
  Lcd_Write_Data(0X81); 

  Lcd_Write_Com(0xF7);  
  Lcd_Write_Data(0x20); 
  
  Lcd_Write_Com(0xC0);    //Power control 
  Lcd_Write_Data(0x23);   //VRH[5:0] 
 
  Lcd_Write_Com(0xC1);    //Power control 
  Lcd_Write_Data(0x10);   //SAP[2:0];?  BT[3:0] 

  Lcd_Write_Com(0xC5);    //VCOM control 1
  Lcd_Write_Data(0x3e);   // VMH[6] Contrast
  Lcd_Write_Data(0x28);   // VML[6] 
 
  Lcd_Write_Com(0xC7);    //VCOM control 2 
  Lcd_Write_Data(0x86);   // VMF[6]
 
  Lcd_Write_Com(0x36);    // Memory Access Control 
  Lcd_Write_Data(0x48);   // MY=0, MX=1, MV=0, ML=0, BGR=1, MH=0, X=0, X=0

  Lcd_Write_Com(0x3A);   // Pixel Format Set  
  Lcd_Write_Data(0x55);  // X=0, DPI[3]=101, X=0, DBI[3]=101

  Lcd_Write_Com(0xB1);   // Frame Control (in Normal Mode) 
  Lcd_Write_Data(0x00);  // X, X, X, X, X, X, DIVA[2]=00 (Division Rate = 0)
  Lcd_Write_Data(0x18);  // X, X, X, RTNA[5]=11000 (Frame Rate = 79Hz)
 
  Lcd_Write_Com(0xB6);    // Display Function Control 
  Lcd_Write_Data(0x08);   // X, X, X, X, PTG[2]=10, PT[2]=00
  Lcd_Write_Data(0x82);   // REV=1, GS=0, SS=0, SM=0, ISC[4]=0010
  Lcd_Write_Data(0x27);   // X, X, PCDIV[6]=100111

  Lcd_Write_Com(0x11);    //Exit Sleep // Sleep OUT
  delay(120); 
				
  Lcd_Write_Com(0x29);    //Display ON
  Lcd_Write_Com(0x2c);    // Memory Write
}

#define PUTPIXEL {                                                \
      GPIOB_BASE->ODR = CTRL_MASK_WR_SC | ((c >> 5) & DATA_MASK); \
      GPIOB_BASE->BSRR = LCD_WR;                                  \
      GPIOB_BASE->ODR = CTRL_MASK_WR_SC | ((c << 3) & DATA_MASK); \
      GPIOB_BASE->BSRR = LCD_WR;                                  \
}

#define PUT_8_PIXELS { \
      PUTPIXEL        \
      PUTPIXEL        \
      PUTPIXEL        \
      PUTPIXEL        \
      PUTPIXEL        \
      PUTPIXEL        \
      PUTPIXEL        \
      PUTPIXEL        \
}
void H_line(unsigned int x, unsigned int y, unsigned int l, unsigned int c)                   
{	
  Address_set(x, y, x+l-1, y);
  for(unsigned int i=0; i<l; i++)
  {
      PUTPIXEL
  }
}

void V_line(unsigned int x, unsigned int y, unsigned int l, unsigned int c)                   
{	
  Address_set(x, y, x, y+l-1);
  for(unsigned int i=0; i<l; i++)
  { 
      PUTPIXEL
  }
}

void Rect(unsigned int x,unsigned int y,unsigned int w,unsigned int h,unsigned int c)
{
  H_line(x    , y    , w, c);
  H_line(x    , y+h-1, w, c);
  V_line(x    , y    , h, c);
  V_line(x+w-1, y    , h, c);
}

void Rectf(unsigned int x,unsigned int y,unsigned int w,unsigned int h,unsigned int c)
{
  unsigned int i;
  for(i=0; i<w; i++)
  {
    V_line(x+i , y, h, c);
  }
}

void LCD_Clear_1px(unsigned int c)
{	
  Address_set(0,0,239,319);
  for(unsigned int i=0; i<(320*240); i++){  //  для  2 пикселей за итерацию
      PUTPIXEL
  }
}

void LCD_Clear_8px(unsigned int c)
{  
  Address_set(0,0,239,319);
  for(unsigned int i=0; i<(40*240); i++){  //  для  8 пикселей за итерацию
    PUT_8_PIXELS
  }
}

void LCD_Clear_64px(unsigned int c)
{  
  Address_set(0,0,239,319);
  for(unsigned int i=0; i<(5*240); i++){   //  для 64 пикселей за итерацию
    PUT_8_PIXELS
    PUT_8_PIXELS
    PUT_8_PIXELS
    PUT_8_PIXELS
    PUT_8_PIXELS
    PUT_8_PIXELS
    PUT_8_PIXELS
    PUT_8_PIXELS
  }
}

void setup()
{
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect.
  }
// default  1     1     1     1     1     x     x    x    x    x    x    x    x    -   1    1
// mode     3     3     3     3     3     3     3    3    3    3    3    3    3    4   7    7
  GPIOB_BASE->ODR = CTRL_MASK_DEFAULT;  // все управляющие HIGH
  GPIOB_BASE->CRL = 0x33333477;         // пины PB7-PB3 - выход, PB2 - вход (не исп.), PB1-PB0 - выход с ОК (I2C)
  GPIOB_BASE->CRH = 0x33333333;         // пины PB15-PB8 - выход
  GPIOB_BASE->ODR = CTRL_MASK_DEFAULT;  // 

  Lcd_Init();
  LCD_Clear_1px(0xffff);
}

void loop()
{ 
  long t0 = micros();
  LCD_Clear_1px(0x07E0); // GREEN
  long t1 = micros();
  delay(500);
  long t2 = micros();
  LCD_Clear_1px(0x001F); // BLUE
  long t3 = micros();
  delay(500);
  long t4 = micros();
  LCD_Clear_1px(0xf800); // RED
  long t5 = micros();
  delay(500);
  int avgTime;
  Serial.print("Fill Screen x1 pixel:   ");
  avgTime = ((t1-t0) + (t3-t2) + (t5-t4) + 1) /3;
  Serial.print(avgTime);
  Serial.print(" us, FPS = ");
  Serial.print(1000000.0/avgTime);
  Serial.print(", (");
  Serial.print(1.0/avgTime*320*240);
  Serial.println(") Mpix/s");
  
  t0 = micros();
  LCD_Clear_8px(0x07E0); // GREEN
  t1 = micros();
  delay(500);
  t2 = micros();
  LCD_Clear_8px(0x001F); // BLUE
  t3 = micros();
  delay(500);
  t4 = micros();
  LCD_Clear_8px(0xf800); // RED
  t5 = micros();
  delay(500);
  Serial.print("Fill Screen x8 pixels:  ");
  avgTime = ((t1-t0) + (t3-t2) + (t5-t4) + 1) /3;
  Serial.print(avgTime);
  Serial.print(" us, FPS = ");
  Serial.print(1000000.0/avgTime);
  Serial.print(", (");
  Serial.print(1.0/avgTime*320*240);
  Serial.println(") Mpix/s");
  
  t0 = micros();
  LCD_Clear_64px(0x07E0); // GREEN
  t1 = micros();
  delay(500);
  t2 = micros();
  LCD_Clear_64px(0x001F); // BLUE
  t3 = micros();
  delay(500);
  t4 = micros();
  LCD_Clear_64px(0xf800); // RED
  t5 = micros();
  delay(500);
  Serial.print("Fill Screen x64 pixels: ");
  avgTime = ((t1-t0) + (t3-t2) + (t5-t4) + 1) /3;
  Serial.print(avgTime);
  Serial.print(" us, FPS = ");
  Serial.print(1000000.0/avgTime);
  Serial.print(", (");
  Serial.print(1.0/avgTime*320*240);
  Serial.println(") Mpix/s");

  for(int i=0;i<100;i++)
  {
    unsigned int x = random(220);
    unsigned int y = random(300);
    unsigned int w = random(240 - x);
    unsigned int h = random(320 - y);
    unsigned int c = random(65535);
    Rect(x,y,w,h,c); // rectangle at x, y, with, hight, color
  }
  long t8 = micros();
      Rect(0, 0, 240, 320, 0xffff);
      Rect(1, 1, 238, 318, 0x001f);
      Rect(2, 2, 236, 316, 0x001f);
      Rect(3, 3, 234, 314, 0xffff);
  delay(2000); 
}

Как выяснилось, дисплей оптимизирован под групповые операции, т.е. можно определить прямоугольный регион, а дальше просто писать туда данные, и он будет последовательно (строка за строкой) заполняться.

Общие результаты таковы:

Скорость заполнения (1 пиксель на итерацию)  : 5.52 Мпикс/с (FPS= 71.9).
Скорость заполнения (8 пикселей на итерацию) : 8.32 Мпикс/с (FPS=108.3).
Скорость заполнения (64 пикселя на итерацию) : 8.88 Мпикс/с (FPS=115.6).
 
Естественно, это максимальная скорость - заливки одним цветом. При выводе картинки или текста скорость, естественно, будет пониже, но, думаю, все равно можно добиться хороших результатов. А вот попиксельный вывод неизбежно будет очень медленным. Что, собственно, во всех стандартных библиотеках мы и наблюдаем. Это стандартная ошибка - сначала свести все к общему знаменателю, в данном случае - к PutPixel, а потом этим путпикселем рисовать все, что только можно. В результате все аппаратные ухищрения конструкторов по ускорению работы их изделия идут коту под хвост.
Попутно выяснилось, что далеко не все благополучно с оптимизацией кода. Из 5 вариантов, представленных в Arduino IDE для stm32f103c только один сумел выбросить из цикла все лишнее. Причем, им оказался -O1, т.е. не самая жесткая оптимизация по мнению разработчиков.
a5021
Offline
Зарегистрирован: 07.07.2013

Проверьте -Ofast. Я вроде это уже  советовал.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Спасибо, дойдут руки - проверю. Но для этого надо ковырять IDE, как там провесить команды, не выведенные в меню. Пока что есть более интересное занятие.

И, кстати, мне не попадалась таблица команд с растактовкой для stm32f103c. Ни у кого нет подходящей ссылочки?

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015
Что-то делать интереснее, чем это потом описывать.
Поэтому описание идет с некоторым запаздыванием. Что дает возможность в описании несколько "заглянуть вперед".
Так было и с SCCB: я написал, что с ним проблем не было. Но это не значит, что все сразу пошло гладко. Нет, сначала, наоборот, - ничего не пошло. Т.е. на внешние команды камера никак не реагировала. Ответ всегда был 0xFF, что вполне естественно, учитывая что протокол подразумевает "монтажное И" или "схему с открытым коллектором".
Оказалось, что на камеру обязательно надо подавать внешнюю частоту, которую собственно камера использует для формирования пиксельной частоты. Оказалось, что без этой частоты не работает не только собственно "видеовыход", но и управляющий канал - SCCB. 
В документации указывается диапазон допустимых внешних частот: от 10 до 48 МГц. Ну, в нашем случае, понятно, частоты нужны поменьше: нужно поймать синхронизацию, считать данные, куда-то их записать, ну и проверка/переход на продолжение/окончание цикла. И все это - 2 раза на пиксель: глубина цвета-то 16-разрядная: RGB565.
Выяснилось, что при снижении внешней частоты SCCB сначала начинает делать ошибки, а потом постепенно и совсем уходит в 0xFF. По моим оценкам, частота 10 МГц заявлена не просто так, а исходя из максимальной частоты SCCB в 400 кГц. Я реализовывал SCCB на 100 кГц. И по моим оценкам граница работоспособности и пролегает где-то в районе внешней частоты 2.5 МГц. Но точно я это не выяснял, т.к. и хлопотно и незачем.
Камера допускает установку делителя входного сигнала (собственно, умножителя - тоже, но я этим не пользовался). В принципе, можно установить делитель побольше, и тогда в более широком диапазоне камера будет оставаться работоспособной (точнее, ее канал управления - SCCB). Но тогда - большая дискретность задания частоты, т.к. последняя может получаться только делением 72 МГц контроллера. Ну а при уменьшении делителя "ступеньки" регулировки пиксельной частоты уменьшаются, но уменьшается и возможный диапазон регулировки вниз из-за отказа SCCB.
В общем, опять же, забегая вперед, скажу, что "оптимум", которого удалось добиться, это 4 МГц внешней частоты, делитель на 2 и, соответственно, частота, с которой выдаются данные - 2 МГц, что соответствует 1 Мпикселю в секунду.
В принципе, при 320*240=76800 и пиксельной частоте 1 МГц, казалось бы, можно добиться примерно 13 FPS. Но не тут то было.
Еще при первом просмотре дэйташита меня насторожил рисунок:
но, как-то пропустил мимо внимания - не может же быть все так плохо. Оказалось - может!
(попутно отмечу: на рисунке отчетливо видна надпись "see Figure 7". Так вот, читать следует "see Figure 6". И так - весь дэйташит!)
На рисунке: положительная часть импульса - строка, а отрицательная (нулевая) - импульс гашения.
Т.е. в режиме 240 строк половина строчных импульсов просто пропускается, в результате чего длина импульса гашения (который нам абсолютно не нужен) оказывается более чем вдвое больше, чем длина строки. Другими словами: при одном и том же FPS при уменьшении количества пикселей в 4 раза пиксельная частота снижается только вдвое!
Приведу заодно и процитированный выше рисунок 6:
Оказывается, что те цифры, которые на нем указаны, уменьшены быть не могут. Увеличены - да (если нам надо точно подогнать FPS под заданную величину, можно добавить несколько фиктивных пикселей в строку или несколько фиктивных строк в кадр), а уменьшены - нет.
При формате кадра 640*480 полный размер экрана с зонами гашения оказывается 784*510. Т.е. кадр занимает в экране 640*480/(784*510)=76.8%. При 320*240 - существенно хуже: 320*240/(392*510)=38.4%.
В принципе, хорошим выходом мог бы быть interleaving режим развертки с двумя полукадрами по 240 строк (кстати, именно таков стандартный сигнал телевидения в формате NTSC), но, увы, эта камера, похоже, такой не поддерживает.
ssss
Offline
Зарегистрирован: 01.07.2016

andriano пишет:

Попутно выяснилось, что далеко не все благополучно с оптимизацией кода. Из 5 вариантов, представленных в Arduino IDE для stm32f103c только один сумел выбросить из цикла все лишнее. Причем, им оказался -O1, т.е. не самая жесткая оптимизация по мнению разработчиков.

Значит... либо ардуина шалит... либо с ключами ЖЦЦ не разобрались... На Кейле всё нормально... начиная с -O1 и выше...

ssss
Offline
Зарегистрирован: 01.07.2016

andriano пишет:

И, кстати, мне не попадалась таблица команд с растактовкой для stm32f103c. Ни у кого нет подходящей ссылочки?

здесь

http://infocenter.arm.com/help/topic/com.arm.doc.ddi0337e/DDI0337E_cortex_m3_r1p1_trm.pdf

Глава 18

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Спасибо.

Ну а с ключами gcc, а также вообще с тем, как до них добраться через Arduino IDE, я действительно разбираться даже не начинал.

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

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015
При попытках настроить оптимальную передачу изображения с камеры на дисплей тоже столкнулся с массой интересного.
Камера может формировать не только сигнал с матрицы, но и испытательную картинку - цветные полосы либо цветные полосы, переходящие в серый клин. С этого режима и начал.
Уже - что-то.
Фотографирование экрана с последующим подсчетом количества помех и простенькими арифметическими вычислениями привели меня к оценке периодичности возникновения помехи - примерно 1000 раз в секунду (либо 1000000/1024, что с учетом точности оценки одно и то же). Вероятнее всего это прерывание millis(). Как оно устроено в Atmel328, я знаю, а вот как в stm32f103c - увы.
В общем, поступил просто: на время сигнала строки запретил прерывания, а после его окончания - разрешил.
Уже гораздо лучше.
 
Кроме того, это мой первый опыт работы с логическим анализатором. В свое время купил его, попробовал, но как-то всегда хватало осциллографа. Даже для отладки цифровых протоколов, например, с чипом M62429 ( http://arduino.ru/forum/proekty/analog-analogovogo-sintezatora#comment-2... ), вполне обходился осциллографом. Но для видеосигнала только двух "лучей" и весьма короткого периода записи сигнала оказалось недостаточно. Да и временнЫе характеристики тоже желательно было знать точнее, чем показывает осциллограф.
Тут тоже оказалось много интересного. Например, при подсчете количества импульсов записи (на дисплей), которое, как не трудно догадаться, равно количеству принятых байтов с камеры, у меня получались величины порядка 430-440. Вместо 640. Еще раз просмотрел собственный код: цикл до 640. Если бы не успевал за длительность кадрового импульса, "хвост" пачки импульсов записи просто уходил бы за пределы строчного импульса, читая байты там, где камера уже выдавала импульс гашения. Благо, пиксельная частота по умолчанию не отключена в периоды гашения, и менять это я не стал. 
Оказалось, следующее: максимальная частота дискретизации логического анализатора 24 МГц, что соответствует трем периодам тактовой частоты контроллера (72 МГц / 24 МГц = 3 периода). А длительность импульса записи - только 2 периода. Поэтому вероятность того, что логический анализатор сумеет его обнаружить, составляет 2/3 или 66.7%. Вот я и получал 426 импульсов вместо 640.
В общем, нужен логический анализатор пошустрее.
Далее. Собственно, картинки я привел, а получил их я далеко не сразу. Помимо прочего, оказалось, что для получения такой картинки нужно пропустить первый байт изображения. Вероятно, просто камера передает байты в последовательности обратной той, что нужна дисплею. В самой камере байты переставить нельзя, можно - только биты внутри байта. Но это не то, что нужно. Вроде, перестановка байтов возможна при настройке дисплея, но что-то с первой попытки у меня это не получилось, а дальше пробовать не стал. По сути, это значит, что цвет пикселя берется один байт от одного пикселя, а другой - от соседнего. На цветных полосах этот дефект заметен,  а на картинке - практически нет. Пока оставил так.
andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015
Пытался применять различные частоты тактирования.
Остановился в МК на делителе 1 (т.е. вдвое), длине полупериода - 1 и прескаляре 8 (т.е. в 9 раз). Итого, выходная частота 72/9/2=4 МГц. Камера делит ее вдвое и формирует частоту передачи данных 2 МГц, что соответствует 1 Мпикс/сек. При этом FPS получается 1000000/(510*392)= 5.002, что и наблюдается на деле.
В цикле обработки пикселя нужно:
1. дождаться "0" синхросигнала (2 МГц), иначе можем повторно прочитать предыдущий пиксель,
2. дождаться "1", которая сигнализирует о готовности данных, иначе можем читать данные в момент их изменения,
3. прочитать данные из порта ввода, у меня это PortA,
4. наложить на них максу данных, чтобы отсечь ненужные управляющие сигналы,
5. наложить маску нужных управляющих сигналов,
6. вывести то, что получилось, в порт вывода, у меня это PortB,
7. перевести из "0" в "1" управляющий сигнал записи,
8. декренментировать счетчик переданых байтов,
9. проанализировать на равенство 0 и при отрицательном результате уйти на начало цикла.
Под "дождаться" на самом деле подразумевается три действия: чтение порта, сравнение, условный переход. И, возможно, не один раз - пока дождешься.
И все это - за 36 тактов, если исходить из 5 FPS. А компилятор норовит еще напихать в этот цикл операций загрузки в регистры масок и номеров регистров, хотя они в процессе выполнения цикла не меняются.
В принципе, можно еще перенести ожидание "0" в конец цикла, что было сделано и привело к большей устойчивости картинки на 5 FPS (иногда наблюдались помехи). Но при прескалере 7 и FPS=5.6 помехи все равно появлялись.
Можно вообще отказаться от ожидания "0", считая, что пока мы сделаем все, что нужно, "1" все равно уже уйдет и обработка двух байт за один период в принципе неосуществима. С одной стороны, это позволило получать узнаваемое изображение вплоть до прескалера 4 (FPS=9.0), против минимального 7 в обычном режиме, но картинка всегда получалась с помехами.
Еще попытался вынести запись в дисплей вообще из цикла, заменив его записью в строчный буфер. При этом вместо 4-7 окажется нужно только единственная операция записи в память при том, что адрес определяется уже существующим счетчиком цикла. Но выигрыш, если и был, оказался невелик, - добиться устойчивой картинки без помех для FPS выше 5 все равно не получилось.
Ассемблерный код и возможность более глубокой оптимизации на этом этапе не рассмативал.
 
andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015
Ну ладно, пока будем исходить из 5 FPS, а там - посмотрим.
Пора приступать к получению и выводу реального изображения с матрицы.
И здесь меня ждал облом - при отключении испытательной таблицы цветных полос изображение не появлялось.
В конце концов, проштудировав кроме документации еще и выложенные в И-нет коды подобных проектов, обнаружил, что люди пишут в регистр 0x73 величину 0xf1, тогда как согласно дэйташиту старшие 4 бита этого регистра зарезервированы и их значение по умолчанию - 0.
Опытным путем выяснилось, что для получения изображения необязательно заполнять все 4 бита, достаточно установить в "1" один старший (d7).
После этого изображение появилось:
Контрастность - выше всяких похвал (но ниже всякой критики). Причем, интересно, что сначала изображение появляется более или менее вменяемое, а потом за несколько кадров контрастность увеличивается до совершенно неприемлемого значения. Впрочем, если вести речь о компьютерном зрении и для обработки превращать картинку в черно-белую (без полутонов), может, оно и к лучшему.
Но пока захотелось иметь вменяемое (с точки зрения человеческого глаза) изображение.
После длительного перебора помог регистр 0xaa, который в одном из вариантов дэйташита значится просто:
9F-AB _without_name_ XX - Histogram-based AEC/AGC Control
т.е. названия эти регистры не имеют, значения по умолчанию - тоже, для чтения они предназначены или для записи, - неизвестно, в общем, обходись как хочешь.
Хорошо, что мне удалось найти 3 разных варианта дэйташита, и в одном из них этот регистр был описан. (Чего, кстати, нельзя сказать о упомянутом выше регистре 0x73, описания которого мне найти так и не удалось)
В общем, изображение стало напоминать старый добрый (ламповый) черно-белый телевизор. Изображение практически с нулевой цветовой насыщенностью. А там, где проглядывает что-то хоть чуть-чуть цветное, например, ярко красная кнопка, камера показывает… грязный оттенок зеленого.
Где-то в словесном описании (нет, кроме шуток, пока нашел 3 варианта pdf с дэйташитом, (это три разных варианта, каждый из них обнаружился еще в куче мест) вариантов рекламного описания пришлось просмотреть море!) фигурировало, что камера допускает регулировку и цветового тона, и насыщенности. Но в дэйташите этих терминов не обнаружил даже поиском.
В принципе, есть матрица преобразования цветоразностного сигнала в цветной  из 6 чисел. И есть формула, как эту матрицу использовать. Можно, конечно, потыкаться, но при хорошей имитации черно-белого ТВ как-то появляются сомнения в возможности вытащить цвет. Я даже стал подозревать авторов, которые якобы изначально пытались получить с камеры именно черно-белый сигнал, - а не приняли ли они решение о "черно-белости" только после того, как не сумели получить вменяемого цветного? Ну и скромно умолчали об этом - будто так и планировалось.
В общем, вот как камера видит спектр:
Ну и в заключение - последний вариант скетча. Возможно, я еще вернусь к вопросу цвета, но пока на некоторое время поставлю точку. 
#include "sccb.h"
#include "ili9341_a.h"

#define PA0_In    (*((volatile unsigned long *) 0x42210100 )) // HS
#define PA1_In    (*((volatile unsigned long *) 0x42210104 )) // PCLK
#define PA15_In   (*((volatile unsigned long *) 0x4221013C )) // VS
#define PB12_Out  (*((volatile unsigned long *) 0x422181B0 )) // LCD_CS
#define PB13_Out  (*((volatile unsigned long *) 0x422181B4 )) // LCD_RS
#define PB14_Out  (*((volatile unsigned long *) 0x422181B8 )) // LCD_WR
#define PC13_Out  (*((volatile unsigned long *) 0x422201B4 )) // LED

cSCCB sccb;

struct regval_list{
  uint8_t reg_num;
  uint16_t value;
};

const struct regval_list  QVGA_A [] = {
    {0x12, 0x04},      // COM7  : RGB format D1 - color bar
    {0x40, 0xD0},      // COM15 : RGB 5:6:5
    {0x0c, 0x04},      // COM3  : enable downsamp/crop/window COM3_DCWEN ([6]=MSB LSB swap)def=0

    {0x3e, 0x19},      // COM14 : DCW_Scale | manually_scaling_parameter | PCLK_divide_by_2
    {0x72, 0x11},      // SCALING DCWCTR : vertical_down_sample_by_2 | horizontal_down_sample_by_2
    {0x73, 0x81},      // SCALING PCLK_DIV : def=f1 !!!! | clock divider by 2 !!! бит d7 включает камеру!!!
    {0x17, 0x16},      // HSTART : horizontal frame start 22*8 + ... (176+) def=11, было=16
    {0x18, 0x04},      // HSTOP : horizontal frame end 4*8 + ... = (32+)   def=61, было=04
    {0x32,0x24},       // HREF : ?0xa4? [32] [17]+4, [18]+4
    {0x19,0x02},       // VSTART : 2*4 + ... (8+)                           def=03 ,было=02
    {0x1a,0x7a},       // VSTOP : 122*4 + ... (488+)                       def=7b, было=7a
    {0x03, 0x0a},      // VREF : [19]+2, [1a]+2
//    {0x70, 0xca},      // SCALING XSC : добавляет к color bar серый клин
//    {0x71, 0xb5},      // SCALING YSC : for 8-bar color bar

//    {0x2b, 0x08},      // EXHCL : dummy pixel in horiz direction - добавляет в конец?
//    {0x30, 0x10},      // HSYST : rising H delay (default=8)
    {0xaa, 0x94},      // HAECC7 : histogram - instead 0x14
//    {0xb1, 0x04},      // ABLC1 : auto black level (00) - не заметно
//    {0x6f, 0x90},      // AWBCTR0 : AutoWhiteBalance mode, (9a) - не заметно
//    {0x01, 0x80},      // BLUE : Blue Gain
//    {0x02, 0xc0},      // RED : Red Gain
//    {0x6a, 0x4f},      // GGAIN : Green gain
    
    {0xff, 0xff},  // END MARKER 
}; 

void wrSensorRegs8_8(const struct regval_list reglist[]){
  uint8_t reg_addr, reg_val;
  const struct regval_list *next = reglist;
  while ((reg_addr != 0xff) | (reg_val != 0xff)){
    reg_addr = next->reg_num;
    reg_val = next->value;
    sccb.writeRegister(reg_addr, reg_val);
    next++;
  }
}

void timer2ch3_init(){ // timer 2 channel 3
    RCC_BASE->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN;     // enable port A and AFIO peripherals
    RCC_BASE->APB1ENR |= RCC_APB1ENR_TIM2EN;     // enable TIM4 peripheral
    GPIOA_BASE->CRL = (GPIOA_BASE->CRL & ~(0x0F<<8)) | (0x0B<<8); // (*)  // configure A2 to alternative output mode
// set up timer
    TIMER2_BASE->CR1 = (1 << 7);     // TIM_CR1_ARPE;    // count up to ARR, no divisor, auto reload
    TIMER2_BASE->ARR = 1;            // period
    TIMER2_BASE->PSC = 8;            // prescaler
    TIMER2_BASE->EGR = 1;            // TIM_EGR_UG;        // generate an update event to reload the prescaler value immediately
// set up compare (ch3)
    TIMER2_BASE->CCER &= ~(0x0300);  // (*)~(TIM_CCER_CC3E | TIM_CCER_CC3P); // disable cc3 output, clear polarity
    TIMER2_BASE->CCR3 = 1;           // (*) cc3 period
    TIMER2_BASE->CCMR2 &= 0xFF00;    // (*)
    TIMER2_BASE->CCMR2 |= 0x0068;    // (*)

    TIMER2_BASE->CCER |= 0x0100; // (*) TIM_CCER_CC3E;    // enable cc4 output
    TIMER2_BASE->CR1 |= 1; //TIM_CR1_CEN;        // run timer
}

void setup(){
  GPIOC_BASE->CRH = 0x44344444;     // PC13 olly - LED
// default  1     1     1     1     1     x     x    x    x    x    x    x    x    -   1    1
// mode     3     3     3     3     3     3     3    3    3    3    3    3    3    4   7    7
  GPIOB_BASE->ODR = INV_DATA_MASK;  // 0xf103
  GPIOB_BASE->CRL = 0x33333477;     // пины PB7-PB3 - выход, PB2 - вход (не исп.), PB1-PB0 - выход с ОК
  GPIOB_BASE->CRH = 0x33333333;     // пины PB15-PB8 - выход
  GPIOB_BASE->ODR = INV_DATA_MASK;  // 0xf103

  Lcd_Init();
  LCD_Clear(0xffff);    
  digitalWrite(PC13, HIGH);
  timer2ch3_init();
  delay(300);
  sccb.writeRegister(0x12, 0x80);
  delay(200);
  wrSensorRegs8_8(QVGA_A); // QVGA_A
  delay(100);
}

void loop(){
// VSYNC  
  while (!PA15_In);//wait for high  VS
  while (PA15_In);//wait for low  VS
  int y = 240;
  static uint16_t buff[640]; // буфер для строки
  while (y--){  
     int x = 640;  
     Address_set(y,0,y,319);
     PB13_Out = 0;
     noInterrupts();
     PB13_Out = 1;
     while (PA0_In);//wait for low HS  
     while (!PA0_In);//wait for high HS  
        while (PA1_In);//wait for low    PCLK
        while (!PA1_In);//wait for high    PCLK
     while (x--){  
      //PCLK  
        while (!PA1_In);//wait for high    PCLK
        buff[x] = GPIOA_BASE->IDR;                                 //  вариант с буфером строки
//        GPIOB_BASE->ODR = 0xa803 | (GPIOA_BASE->IDR & DATA_MASK);   //  вариант 
//        GPIOB_BASE->BRR = LCD_WR;                                   //    без
//        GPIOB_BASE->BSRR = LCD_WR;                                  //  буфера строки
        while (PA1_In);//wait for low    PCLK
     }  
     PB13_Out = 0;
     interrupts();
     PB13_Out = 1;
     x = 640;                                              //
     while (x--){                                          // вариант
        GPIOB_BASE->ODR = 0xa803 | (buff[x] & DATA_MASK);  //    с
        GPIOB_BASE->BRR = LCD_WR;                          // буфером
        GPIOB_BASE->BSRR = LCD_WR;                         //  строки
     }                                                     //
  }
}

 

И включаемый файл (правда, слепленный не по фен-шую)

#ifndef ILI9341_A_H
#define ILI9341_A_H

#include <Arduino.h> 

#define LCD_RD        0x8000 // PB15   A0 - не используется (если использовать, нужно согласовать уровни сигналов)
#define LCD_WR        0x4000 // PB14   A1
#define LCD_RS        0x2000 // PB13   A2
#define LCD_CS        0x1000 // PB12   A3       
#define LCD_RESET     0x0800 // PB11   A4
#define DATA_MASK     0x07f8 // PB3-PB10 (8, 9, 2-7)
#define INV_DATA_MASK 0xf803 // бит PB2 не используется, поэтому не 0xf107

// pin     PB15  PB14  PB13  PB12  PB11  PB10  PB9  PB8  PB7  PB6  PB5  PB4  PB3  PB2  PB1  PB0
// func     RD    WR    RS    CS   REST   D7    D6   D5   D4   D3   D2   D1   D0   -   SDA  SCL
// default  1     1     1     1     1     x     x    x    x    x    x    x    x    -   1    1
// mode     3     3     3     3     3     3     3    3    3    3    3    3    3    4   7    7

inline void myDelay(volatile unsigned long n) { while (n--); }

inline void selectLCD() { GPIOB_BASE->BRR = LCD_CS; /*myDelay(10);*/ }

inline void releaseLCD() { GPIOB_BASE->BSRR = LCD_CS; /*myDelay(10);*/ }
 
inline void Lcd_Write_Com(unsigned char d)  // вывод команды: опустить PB13, вывести байт, поднять PB13
{   
  GPIOB_BASE->ODR = 0x8803 | (d << 3); //~LCD_WR; // 0xe803
  GPIOB_BASE->BSRR = LCD_WR;
}

inline void Lcd_Write_Data(unsigned char d)
{
  GPIOB_BASE->ODR = 0xa803 | (d << 3); //~LCD_WR; // 0xe803
  GPIOB_BASE->BSRR = LCD_WR;
}

void Address_set(unsigned int x1,unsigned int y1,unsigned int x2,unsigned int y2)
{
/*  Lcd_Write_Com(0x2a);    // Column Address Set
  Lcd_Write_Data(x1>>8);  // SC[15:8]
  Lcd_Write_Data(x1);     // SC[7:0]
  Lcd_Write_Data(x2>>8);  // EC[15:8]
  Lcd_Write_Data(x2);     // EC[7:0]
 
  Lcd_Write_Com(0x2b);    // Page Address Set
  Lcd_Write_Data(y1>>8);  // SP[15:8]
  Lcd_Write_Data(y1);     // SP[7:0]
  Lcd_Write_Data(y2>>8);  // EP[15:8]
  Lcd_Write_Data(y2);     // EP[7:0]
  Lcd_Write_Com(0x2c);    // Mempry Write */

  GPIOB_BASE->ODR = 0x8953; // Write_Com(0x2a); -Column Address Set //INV_DATA_MASK & ~LCD_WR & ~LCD_RS & ~LCD_CS | (0x2a << 3)  /// (0010 1010 -> 1 0101 0000 = 0x150) => 
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = 0xa803; // считая, что x1 <= 240 | ((x1 & 0xff00)>>5); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = 0xa803 | ((x1 & 0xff) << 3); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = 0xa803; // считая, что x2 <= 240 | ((x2 & 0xff00)>>5); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = 0xa803 | ((x2 & 0xff) << 3); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = 0x895b; // Write_Com(0x2b);    // Page Address Set
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = 0xa803 | ((y1 & 0xff00)>>5); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = 0xa803 | ((y1 & 0xff) << 3); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = 0xa803 | ((y2 & 0xff00)>>5); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = 0xa803 | ((y2 & 0xff) << 3); //
  GPIOB_BASE->BSRR = LCD_WR;

  GPIOB_BASE->ODR = 0x8963; // Write_Com(0x2c);    // Mempry Write // 2c = 0010 1100 -> 1 0110 0000 -> 0x160
  GPIOB_BASE->BSRR = LCD_WR;
}

void Lcd_Init(void)
{
  GPIOB_BASE->BSRR = LCD_RESET;
  delay(5); 
  GPIOB_BASE->BRR = LCD_RESET;
  delay(15);
  GPIOB_BASE->BSRR = LCD_RESET;
  delay(15);

  releaseLCD(); //GPIOB_BASE->BSRR = LCD_CS;
  delay(1);
  GPIOB_BASE->BSRR = LCD_WR;
  delay(1);
  selectLCD(); //GPIOB_BASE->BRR = LCD_CS;
  delay(1);

  Lcd_Write_Com(0xCB);  
  Lcd_Write_Data(0x39); 
  Lcd_Write_Data(0x2C); 
  Lcd_Write_Data(0x00); 
  Lcd_Write_Data(0x34); 
  Lcd_Write_Data(0x02); 

  Lcd_Write_Com(0xCF);  
  Lcd_Write_Data(0x00); 
  Lcd_Write_Data(0XC1); 
  Lcd_Write_Data(0X30); 

  Lcd_Write_Com(0xE8);  
  Lcd_Write_Data(0x85); 
  Lcd_Write_Data(0x00); 
  Lcd_Write_Data(0x78); 

  Lcd_Write_Com(0xEA);  
  Lcd_Write_Data(0x00); 
  Lcd_Write_Data(0x00); 
 
  Lcd_Write_Com(0xED);  
  Lcd_Write_Data(0x64); 
  Lcd_Write_Data(0x03); 
  Lcd_Write_Data(0X12); 
  Lcd_Write_Data(0X81); 

  Lcd_Write_Com(0xF6);  // bytes order ?????????????????????????????????
  Lcd_Write_Data(0x01); 
  Lcd_Write_Data(0x00); 
  Lcd_Write_Data(0x00); // 0x00

  
  Lcd_Write_Com(0xF7);  
  Lcd_Write_Data(0x20); 
  
  Lcd_Write_Com(0xC0);    //Power control 
  Lcd_Write_Data(0x23);   //VRH[5:0] 
 
  Lcd_Write_Com(0xC1);    //Power control 
  Lcd_Write_Data(0x10);   //SAP[2:0];?  BT[3:0] 

  Lcd_Write_Com(0xC5);    //VCOM control 1
  Lcd_Write_Data(0x3e);   // VMH[6] Contrast
  Lcd_Write_Data(0x28);   // VML[6] 
 
  Lcd_Write_Com(0xC7);    //VCOM control 2 
  Lcd_Write_Data(0x86);   // VMF[6]
 
  Lcd_Write_Com(0x36);    // Memory Access Control 
  Lcd_Write_Data(0x88);   // MY=0, MX=1, MV=0, ML=0, BGR=1, MH=0, X=0, X=0 // было 0x48

  Lcd_Write_Com(0x3A);   // Pixel Format Set  
  Lcd_Write_Data(0x55);  // X=0, DPI[3]=101, X=0, DBI[3]=101

  Lcd_Write_Com(0xB1);   // Frame Control (in Normal Mode) 
  Lcd_Write_Data(0x00);  // X, X, X, X, X, X, DIVA[2]=00 (Division Rate = 0)
  Lcd_Write_Data(0x18);  // X, X, X, RTNA[5]=11000 (Frame Rate = 79Hz)
 
  Lcd_Write_Com(0xB6);    // Display Function Control 
  Lcd_Write_Data(0x08);   // X, X, X, X, PTG[2]=10, PT[2]=00
  Lcd_Write_Data(0x82);   // REV=1, GS=0, SS=0, SM=0, ISC[4]=0010
  Lcd_Write_Data(0x27);   // X, X, PCDIV[6]=100111

//  Lcd_Write_Com(0xF6);  // bytes order ?????????????????????????????????
//  Lcd_Write_Data(0x01); 
//  Lcd_Write_Data(0x00); 
//  Lcd_Write_Data(0x20); // 0x00


  Lcd_Write_Com(0x11);    //Exit Sleep // Sleep OUT
  delay(120); 
        
  Lcd_Write_Com(0x29);    //Display ON
  Lcd_Write_Com(0x2c);    // Memory Write
}

#define PUTPIXEL {                                       \
      GPIOB_BASE->ODR = 0xa803 | ((c >> 5) & DATA_MASK); \
      GPIOB_BASE->BSRR = LCD_WR;                         \
      GPIOB_BASE->ODR = 0xa803 | ((c << 3) & DATA_MASK); \
      GPIOB_BASE->BSRR = LCD_WR;                         \
}

#define PUT_8_PIXELS { \
      PUTPIXEL        \
      PUTPIXEL        \
      PUTPIXEL        \
      PUTPIXEL        \
      PUTPIXEL        \
      PUTPIXEL        \
      PUTPIXEL        \
      PUTPIXEL        \
}
void H_line(unsigned int x, unsigned int y, unsigned int l, unsigned int c)                   
{ 
  selectLCD(); // GPIOB_BASE->BRR = LCD_CS;
  Address_set(x, y, x+l-1, y);
  for(unsigned int i=0; i<l; i++)
  {
      PUTPIXEL
  }
  releaseLCD(); // GPIOB_BASE->BSRR = LCD_CS;
}

void V_line(unsigned int x, unsigned int y, unsigned int l, unsigned int c)                   
{ 
  selectLCD(); //   GPIOB_BASE->BRR = LCD_CS;
  Address_set(x, y, x, y+l-1);
  for(unsigned int i=0; i<l; i++)
  { 
      PUTPIXEL
  }
  releaseLCD(); // GPIOB_BASE->BSRR = LCD_CS;
}

void Rect(unsigned int x,unsigned int y,unsigned int w,unsigned int h,unsigned int c)
{
  H_line(x    , y    , w, c);
  H_line(x    , y+h-1, w, c);
  V_line(x    , y    , h, c);
  V_line(x+w-1, y    , h, c);
}

void Rectf(unsigned int x,unsigned int y,unsigned int w,unsigned int h,unsigned int c)
{
  unsigned int i;
  for(i=0; i<w; i++)
  {
    V_line(x+i , y, h, c);
  }
}

void LCD_Clear(unsigned int c)
{ 
  selectLCD(); //   GPIOB_BASE->BRR = LCD_CS;
  Address_set(0,0,239,319);
  for(unsigned int i=0; i<(40*240); i++){  //  для  2 пикселей за итерацию
    PUT_8_PIXELS
  }
  releaseLCD(); // GPIOB_BASE->BSRR = LCD_CS;
}

#endif

А два остальных файла уже публиковались в сообщении №5.