オタクスタイルの人生のお祝い:ARMとTFT LCD

はじめに



結果 dlinyjgoodic 、およびHoshiからの投稿を見て、Habrはケーキだと改めて感じました。



最初の投稿は、Linux用HD44780に基づいたキャラクターディスプレイドライバーの作成に関するものです( dlinyjから独自のLinuxドライバーを作成する )。 それに対する優れた回答は、グッドハブラウザー (fireを書か ずにオタクスタイルにおめでとう )と新年のラズベリー-HD44780スクリーンをRaspberry Piに固定)の投稿でした



また、この人生のお祝いに参加して、ハードウェアvt52-like



端末を実装したいと考えました。 シンボリックディスプレイはありませんでしたが、本格的な240x320 TFTディスプレイと部分的なドキュメントを備えたARM Cortex-M3に基づいた中国語の開発ボードがありました。



熱意の株があったので、日曜日の午後に目が覚めた(〜17 MSK)、私はこのLCDの組み込みドライバーを書き始めました。



組み込みARMプログラミング、電子機器、または結果だけに興味がある場合は、catの下でお願いします。





自由に使えるのは、USB-to-UART Prolific PL-2303HXハードウェアブリッジを備えたST STM32F103RBマイクロコントローラー多数の小さな周辺機器、および未知の接続図を備えたIlitek ILI9320コントローラーを備えたTFT LCDに基づくCelestial Empireのシンプルなデバッグボード(約20ドル)です。



インサーキットデバッガおよびプログラマとして、 Olimex JTAG ARM-TI​​NY-USB-Hが使用されました 。 良いデバイスであり、 OpenOCDで問題なく動作します



開発者
開発者








より正確には、最初はどのようなコントローラーがLCDにあるのかさえ知りませんでした。 16ビットバスを介して接続されていることをディスプレイモジュールから学習できるすべてのものには、 nWR



nRD



BL_EN



BL_EN



およびRS



信号があります。

その目的は推測するのが難しくなかった:





インターネットの中国セグメントの拡張で見つかったドキュメントのあるアーカイブの1つで、

Ilitek 932xモジュール。



ソフトウェアインターフェース



低レベルインターフェース


RuNetでこのLCDコントローラーを操作することについての説明はあまりないので、おそらく低レベルのインターフェイスについて説明します。



このコントローラーには基本的に4つあります:i80システム(パラレルインターフェイス、従来のメモリ、HD44780インターフェイスに類似)、SPI、VSYNC(システム+ VSYNC



、内部クロック機能付き)およびRGB( VSYNC



HSYNC



ENABLE



、外部機能付き) DOTCLK



タイミング)。 私の場合、i80-systemが利用可能であり、おそらくSPI(チェックしませんでした)。



システムのみを使用したので、その説明を見てみましょう。 記事にあまりロードしないように-スポイラーになります。



電気インターフェイスILI9320
電気レベルでは、デジタルテクノロジーの使用は通常、タイミング図で説明されます。 この例では、5つの制御信号と16ビットのデータバスがあります。



nCS



に情報を送信する前に、 nCS



信号でインターフェイスをアクティブにして、0に設定する必要があります。



さらに、0 RS



に設定すると、情報が記録されるレジスタのアドレスが書き込まれます(実際の記録は、 nWR



信号をアクティブにすることによって実行されますRS



信号は1に戻ります。



その後、実際の読み取りまたは書き込み操作が実行されます(それぞれnRD



nWR



使用)。



これらのプロセスの図は次のとおりです。

op
読む LCD読み取り操作
書きます LCD書き込み操作




GRAMの書き込み/読み取りの際、特殊レジスタ0x22



ます。 さらに、コントローラーは自動インクリメントを行うことができます

GRAMアドレス。その内容を順番に読み書きできます。



チャート:

op
GRAM読み取り LCD GRAM読み取り操作
グラム書き込み LCD GRAM書き込み操作




操作が完了すると、 nCS



1に戻ります。



タイミング図を描くために、ブラウザで動作する素晴らしいwavedromプロジェクトを見つけました。 ここでテストします (上の図はここで準備されました)。




電気インターフェイスに基づいて、低レベルの関数が記述されました。



lcd_ll_funcs
 void _lcd_select(void) { GPIO_ResetBits(GPIOC, GPIO_Pin_9); } void _lcd_deselect(void) { GPIO_SetBits(GPIOC, GPIO_Pin_9); } void _lcd_rs_set(void) { GPIO_SetBits(GPIOC, GPIO_Pin_8); } void _lcd_rs_reset(void) { GPIO_ResetBits(GPIOC, GPIO_Pin_8); } void _lcd_rd_en(void) { GPIO_ResetBits(GPIOC, GPIO_Pin_11); } void _lcd_rd_dis(void) { GPIO_SetBits(GPIOC, GPIO_Pin_11); } void _lcd_wr_en(void) { GPIO_ResetBits(GPIOC, GPIO_Pin_10); } void _lcd_wr_dis(void) { GPIO_SetBits(GPIOC, GPIO_Pin_10); } void _lcd_bl_en(void) { GPIO_SetBits(GPIOC, GPIO_Pin_12); } void _lcd_bl_dis(void) { GPIO_ResetBits(GPIOC, GPIO_Pin_12); } // changes DB[15:0] GPIO pins mode void lcd_gpio_conf(GPIOMode_TypeDef mode); void _lcd_put_data(u16 data) { // data[0-7] -> GPIOC[0-7], data[8-15] -> GPIOB[8-15] GPIOB->ODR = (GPIOB->ODR&0x00ff)|(data&0xff00); GPIOC->ODR = (GPIOC->ODR&0xff00)|(data&0x00ff); } u16 _lcd_read_data(void) { lcd_gpio_conf(GPIO_Mode_IN_FLOATING); u16 result = (GPIOB->IDR&0xff00)|(GPIOC->IDR&0x00ff); lcd_gpio_conf(GPIO_Mode_Out_PP); return result; } // assume that lcd_select() was done before it void _lcd_tx_reg(u8 addr) { _lcd_put_data(addr); _lcd_rs_reset(); _lcd_wr_en(); _lcd_wr_dis(); _lcd_rs_set(); } // assume that _lcd_tx_reg(u8) was done before it void _lcd_tx_data(u16 data) { _lcd_put_data(data); _lcd_wr_en(); _lcd_wr_dis(); } // assume that _lcd_tx_reg(u8) was done before it u16 _lcd_rx_data(void) { _lcd_rd_en(); u16 result = _lcd_read_data(); _lcd_rd_dis(); return result; }
      
      







スピードアップするために、これらの関数をインライン化してマクロに変換することができます(残念ながら、Eclipseはあまりフレンドリーではありません)。



これらの関数に基づいて、レジスターへの書き込み、レジスターからの読み取り、および画像ブリッティングのための関数が実装されています。



高レベルのインターフェース


プログラムの主要部分のLCD機能は、次のAPIを介して利用できます。



 u16 lcd_init(void); void lcd_set_cursor(u16 x, u16 y); void lcd_set_window(u16 left, u16 top, u16 right, u16 bottom); void lcd_fill(u32 color); void lcd_rect(u16 left, u16 top, u16 right, u16 bottom); void lcd_put_char_at(u32 data, u16 x, u16 y); u32 lcd_get_fg(void); u32 lcd_get_bg(void); void lcd_set_fg(u32 color); void lcd_set_bg(u32 color);
      
      







端末機能は、すべての操作にこのインターフェースを使用します。



最も興味深い部分は、フォントのすべての作業がその背後に隠されているため、シンボルを描画する機能です。 次のようになります。



lcd_put_char_at
 void lcd_put_char_at(u32 data, u16 x, u16 y) { u8 xsize, ysize; u8 *char_img; lcd_get_char(data, &xsize, &ysize, &char_img); lcd_set_cursor(x, y); lcd_set_window(x, y, x + xsize, y + ysize); _lcd_select(); _lcd_tx_reg(0x22); // works only for 8xN fonts for(u8 i = 0; i < ysize; i++) { u8 str = char_img[i]; for(u8 j = 0; j < xsize; j++) { _lcd_tx_data((str&(1<<(xsize-j-1)))?fg_color:bg_color); } } _lcd_deselect(); }
      
      







ご覧のとおり、シンボルビットマップへのリンクとそのサイズは、シンボルコードによるlcd_get_char



関数からlcd_get_char



されます(追加の文字でASCII部分に触れないように32ビットです)。



現在、ASCIIテーブルの下部と「ヘリンボーン」を含むフォントが使用されています。 興味のある方は見つけてみてください)



デバッグ
デバッ








最もおもしろくなくて(書き込み時間に関して)最も高価なのは、ディスプレイの初期化関数です。

lcd_init:怖がりたい人向け
 u16 lcd_init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitTypeDef gpio_conf; gpio_conf.GPIO_Speed = GPIO_Speed_50MHz; gpio_conf.GPIO_Mode = GPIO_Mode_Out_PP; gpio_conf.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12; GPIO_Init(GPIOC, &gpio_conf); lcd_gpio_conf(GPIO_Mode_Out_PP); // to init state (0xffff on db0-15, backlit is disabled, nCS, nWR, nRD and RS are high) _lcd_bl_dis(); _lcd_put_data(0xffff); _lcd_deselect(); _lcd_wr_dis(); _lcd_rd_dis(); _lcd_rs_set(); // osc enable _lcd_bl_dis(); lcd_write_reg(0x00, 0x0001); delay_ms(100); u16 lcd_code = lcd_read_reg(0x00); delay_ms(100); // driver output control (S720-S1) lcd_write_reg(0x01, 0x0100); // driving wave control (line inv) lcd_write_reg(0x02, 0x0700); // entry mode (horiz, dir(h+,v+), hwm-, bgr+) lcd_write_reg(0x03, 0x1030); // resize (off) lcd_write_reg(0x04, 0x0000); // display control 2 (skip 2 lines on front porch and on back porch) lcd_write_reg(0x08, 0x0202); // display control 3-4 (scan mode normal, fmark off) lcd_write_reg(0x09, 0x0000); lcd_write_reg(0x0a, 0x0000); // RGB disp iface control (int clock, sys int, 16bit) lcd_write_reg(0x0c, 0x0001); // frame marker position (isn't used) lcd_write_reg(0x0d, 0x0000); // RGB disp iface control 2 (all def, we don't use rgb) lcd_write_reg(0x0f, 0x0000); // power on seq lcd_write_reg(0x07, 0x0021); delay_ms(10); // turn on power supply and configure it (enable sources, set contrast, power supply on) lcd_write_reg(0x10, 0x16b0); // set normal voltage and max dcdc freq lcd_write_reg(0x11, 0x0007); // internal vcomh (see 0x29), pon, gray level (0x08) lcd_write_reg(0x12, 0x0118); // set vcom to 0.92 * vreg1out lcd_write_reg(0x13, 0x0b00); // vcomh = 0.69 * vreg1out lcd_write_reg(0x29, 0x0000); // set x and y range lcd_write_reg(0x50, 0); lcd_write_reg(0x51, LCD_WIDTH-1); lcd_write_reg(0x52, 0); lcd_write_reg(0x53, LCD_HEIGHT-1); // gate scan control (scan direction, display size) lcd_write_reg(0x60, 0x2700); lcd_write_reg(0x61, 0x0001); lcd_write_reg(0x6a, 0x0000); // partial displays off for(u8 addr = 0x80; addr < 0x86; addr++) { lcd_write_reg(addr, 0x0000); } // panel iface control (19 clock/line) lcd_write_reg(0x90, 0x0013); // lcd timings lcd_write_reg(0x92, 0x0000); lcd_write_reg(0x93, 0x0001); lcd_write_reg(0x95, 0x0110); lcd_write_reg(0x97, 0x0000); lcd_write_reg(0x98, 0x0000); lcd_write_reg(0x07, 0x0133); // turn on backlit after init done _lcd_bl_en(); return lcd_code; }
      
      







端末実装



この部分は目立たない。 以前の記事のコードのいくつかを使用して、バッファなしの端末が実装されました。



エスケープシーケンス
エスケープシーケンス:

  • \ 033 [A =カーソルを1行上に移動
  • \ 033 [B =カーソルを1行下に移動
  • \ 033 [C =カーソルを1つ右に移動
  • \ 033 [D =カーソルを1つ左に移動
  • \ 033 [H =カーソルを左上隅に移動-ホーム(位置0,0)
  • \ 033 [J =すべてクリア、カーソルをホームに戻さないでください!
  • \ 033 [K =行末まで消去します。カーソルをホームに戻しません!
  • \ 033 [M =新しい文字マップ-実装されていません
  • \ 033 [Y =位置、YXを取る
  • \ 033 [X =位置、XYを取る
  • \ 033 [R = CGRAMメモリセル選択-CGRAMがないため実装されていません
  • \ 033 [V =スクロールが有効-実装されていません
  • \ 033 [W =スクロールは無効-実装されていません
  • \ 033 [b =バックライトのオン/オフ-実装されていません




その他の便利なコード:

  • \ r =復帰(現在の行の位置0にカーソルを戻します!)
  • \ n =改行
  • \ t =タブ(デフォルトは3文字)






コミュニケーションズ



外の世界と対話するために、 USART1



USB-to-UART PL-2303HX



を介して非同期モードで使用されます。



Linuxホストの観点からは、これは/dev/ttyUSBx



です。 残念ながら、 pl2303



のドライバーはかなり不安定であることが判明しました。 しかし、彼らが拾うとすぐに、彼らはきちんと動作します。



メインループ(空のループ)でUARTをポーリングしないようにするために、割り込みを処理します。



ソフトウェアの観点から見ると、これはUSART1を初期化した後、対応する割り込みベクトルをNVICで設定する必要があることを意味します。



次のようになります。

 NVIC_InitTypeDef nvic_conf; nvic_conf.NVIC_IRQChannel = USART1_IRQn; nvic_conf.NVIC_IRQChannelPreemptionPriority = 0; nvic_conf.NVIC_IRQChannelSubPriority = 2; nvic_conf.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic_conf); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
      
      







最後のコマンドは、受信レジスタUSART1を埋めるイベントを解決します。



したがって、処理は次のようになります。

 void USART1_IRQHandler(void) { u8 data = USART1->DR; uart_write_byte(data); handle_byte(data); }
      
      







バイトを返送(エコー)し、ハンドラーを呼び出します。これは単純なステートマシンです。

handle_byte(u8)
 // escape sequence handling vars u8 escape_seq = 0; u8 buf[10]; void handle_byte(u8 data) { if((!escape_seq) && (data == 0x1b)) { escape_seq = 1; } else if (escape_seq == 1) { buf[escape_seq] = data; escape_seq++; if(data != '[') { escape_seq = 0; } } else if (escape_seq == 2) { switch(data) { case 'A': lcd_term_set_cursor(lcd_term_row()-1, lcd_term_col()); break; case 'B': lcd_term_set_cursor(lcd_term_row()+1, lcd_term_col()); break; case 'C': lcd_term_set_cursor(lcd_term_row(), lcd_term_col()+1); break; case 'D': lcd_term_set_cursor(lcd_term_row(), lcd_term_col()-1); break; case 'H': lcd_term_set_cursor(0, 0); break; case 'J': lcd_term_clear(); break; case 'K': lcd_term_flush_str(); break; case 'X': case 'Y': buf[escape_seq] = data; escape_seq++; return; } escape_seq = 0; } else if(escape_seq == 3) { buf[escape_seq] = data; escape_seq++; } else if(escape_seq == 4) { u8 row = (buf[2] == 'Y') ? buf[3] - 037 : data - 037; u8 col = (buf[2] == 'Y') ? data - 037 : buf[3] - 037; lcd_term_set_cursor(row, col); escape_seq = 0; } else { lcd_term_put_str(&data, 1); } }
      
      







すべてのコードはgithubリポジトリで公開されます。



PS



この投稿の作成には、ほぼ6時間かかりました。 ハードウェアおよびソフトウェア部品の作成とデバッグ-約13時間。



それを読んだすべての人に感謝します。 すべてのナメクジや他の昆虫については、個人的に書きます。



All Articles