STM32およびLCD、クイックフィルスクリーン

現在、さまざまな液晶ディスプレイが広く使用されており、STM32ファミリのコントローラに完全に接続されています。 この記事では、一般的なSTM32F103C8T6コントローラーの1つとSSD1963コントローラーの7インチディスプレイに焦点を当てます。Aliexpressでは比較的完成したノードの形で簡単にアクセスでき、比較的安価です。 。



接続されたデバイスの外観は次のとおりです。



画像



画像



ディスプレイには、製造元の言語のコメント付きの51番目のコントローラーのピン配置と初期化コードが含まれていました。



接続について簡単に



ディスプレイを接続するには、3.3ボルトと5ボルトの電力を必要な端末に供給し、情報ラインをコントローラーに接続します。 制御信号D / C、WE、RSTは、プロセッサの空きI / Oラインに接続されます。 この例では、D / C-PA1、WE-PA8、RST-PA2です。 RDおよびCS信号は省略できますが、論理ユニットをRDに供給する必要があります。 抵抗(この場合4.7 KOhm)を介して+3.3 V、およびCSの「0」に接続します。 アースに接続します。



発言
ディスプレイは、8080モードでインターフェイスを操作するように製造業者によって構成されており、ドキュメントに従って、CS信号「水晶サンプリング」をアクティブにする必要があります。



画像



元々はそのように機能しました。 ただし、テストが示したように、データバスを他の目的に使用したくない場合は必要ありません。



次に、データバスを接続する必要があります。 この表示では、16ビットと想定されていますが、初期化中に8ビットと9ビットの動作モードを選択できます。 つまり、少なくともDB0-DB7表示行と、DB8-DB15の最大行を接続する必要があります。 プログラミングを容易にし、データ変換コマンドを最小化するには、それらを1つのI / Oグループに配置することをお勧めします。 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ビットバスの場合、ピクセルはデータバス上で1つの送信サイクルで送信されます。9ビットバスの場合は2、8ビットバスの場合は3です。このデータの送信元はどこですか。 SSD1963のドキュメントから。







表では、モードに応じて、ピクセルの色の各コンポーネントの位置を見つけることができます。 このプロジェクトでは、8ビット、9ビット、および16ビットモード(565形式)を使用します。 ご覧のとおり、「クリーンな」16ビット形式を使用してより正確なカラーコーディングを行うこともできましたが、バス上での3サイクルのデータ転送も必要です。 ディスプレイ出力には16ビットバスしか存在しないため、18ビット形式と24ビット形式は使用できません。



それでは、プロセッサのディスプレイを72 MHzのクロック周波数でどの速度で満たすことができますか?



176 ms-16ビットバス

374 ms-9ビットバス

470 ms-8ビットバス



もちろん、それほど高速ではありませんが、ゆっくりと変化する情報を表示するには十分かもしれません。 もちろん、16ビットバスはより魅力的に見え、おそらく誰かに合うかもしれませんが、それはあまりにも多くのI / Oポートを占有するため、他のデバイスをプロセッサに接続するには不十分かもしれません。



妥協のオプションを考えてみましょう-9ビットです。8ビットバージョンでは、追加の入力/出力ポートが1つだけであるため、ほぼ0.1秒で勝ちます。



波形




速度の最適化



ディスプレイを埋めるプロセスをスピードアップしてみましょう。 ループ内の論理演算の数を減らすとどうなりますか?



 //   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); } } }
      
      





RGB565形式の16ビット変数の代わりに色分けを変更しました。RGB666形式では18ビットのみを使用して、32ビットを使用します。 さらに、バスに9ビットデータを出力する2サイクル中にLCD_DATA_PORT-> ODRレジスタの値を格納するための一時変数を導入しました。 ここでは、これが常に可能ではないことを予約する必要があります。 出力中、出力用に構成されたGPIO Bグループの他のポートの状態は、その時点で割り込みで変更でき、プログラムは正しく動作しません。 しかし、私たちの場合、そのような問題はなく、達成したことを確認します。 したがって、最初の最適化の後、画面は298ミリ秒で9ビットモードになります。 変数を使用せずにポートの現在の状態を操作する場合、速度は大幅に増加しますが、それほど重要ではありません-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); }
      
      





コードからわかるように、ポートグループBに4つのデータオプションを順番に書き込みます。ここでは、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表示信号を表示し、1サイクルでそれらを考慮することができる場合、制限が少なくなります。



したがって、画面全体または長方形の領域を1色で塗りつぶす最大速度に達しました。 それが限界のようですが、別の制限を導入して、もう少し移動することができます。



実際、一部のアプリケーションでは、ディスプレイに色のセット全体(RGB656〜65536色)が必要ではありません。 たとえば、プロセス制御システムの分野では、生産施設の状態、または何らかのテキストアプリケーション、メッセージの表示を表示する必要があります。 この仮定が正しく、フルカラーの写真およびビデオ素材を表示する必要がない場合、最適化を継続しようとします。

各色がバスを介してディスプレイに送信されるデータの最初と2番目の部分を持つパレットを考えてみましょう。 つまり RGB666モデルの18ビットのうち、最初の9ビットは2番目の9ビットに等しくなります。これにより、2 ^ 9 = 512色が得られます。 おそらく誰かがそれを十分に見つけられないかもしれませんが、英数字情報をプロットまたは表示するには十分かもしれません。 それらを条件付きで「対称色」と呼びます。



対称色
ここに表示されます:







以下は、それらの選択、100個、より明確です:







これらの色のみを使用する理由は何ですか? はい、エリアを埋めるために、塗りプロセス中にデータバスの状態を変更する必要はありません。 WE信号の状態を切り替えて、これを行った回数をカウントするだけで十分です。 さらに、必要に応じてWEを反転することができます。主なことは、領域を埋めるために必要なものより小さくありません。 1つのピクセルに対して1回、バス上で2つのデータブロックを送信する必要があり、その後WE信号による2つの確認が必要であると計算するのは簡単です。 したがって、画面全体に(画面幅*画面長* 2)パルス、または800 * 480 * 2 = 768000が必要です。



インパルスを生成するのがどれほど簡単か。 もちろん! タイマーを使用できます。 このコントローラーのTIM1は、TIM2-TIM4タイマーよりも高速です。 高速なAP​​B2クロックバス上にあります。 調査によると、最小分周器を備えたジェネレーターのPWMモードでタイマーをオンにすると、32 msの充填時間を得ることができます! PA8(TIM1_CH1)など、WE信号をタイマー出力から削除する必要があることは明らかです。



それでも充填速度を上げることはできますか? SYSCC信号をRCC_MCOの出力からWE LCD入力に送るだけで、イエスになりました。 これは、プロセッサで利用可能な最大周波数、72 MHzです。 ディスプレイを対称色で塗りつぶす時間は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) {} }
      
      







タイマーは、32000未満のポイント数については1/72μsの精度で、より多くのポイントについては1μsの精度で時間をカウントします。 これは、タイマーカウンターの長さによるものです。 タイマーがオフになっているときに割り込みを処理するのに時間がかかることを考慮すると、MCO出力の信号は、必要な時間より少し遅れて、わずかなマージンで削除されます。 これは、プロセッサ周波数の約10-11クロックサイクルであることが実験的に確立されています。 したがって、タイマーとRCC_MCOを初期化してシャットダウンするオーバーヘッドにもかかわらず、この手法を使用するためのしきい値があると言えます。 2x2ピクセルの正方形は、おそらくプログラムでループを埋める方がより有益です。



結論として、いくつかの制限を追加することで、画面の充填時間が375ミリ秒から11ミリ秒に短縮されたと言えます。 さらに、プロセッサーの参加なしで充填が行われますが、この時点で他のタスクを実行できます。



私はコメントや追加を喜んでいます。



All Articles