SSブログ
English Version

CH32V203で遊ぶ(その1)SPIでLCD [CH32V]

 円安の影響もあり、従来から愛用してきた PIC24 は値上がりしたままです。「最近遊んだMPU」の記事で書いたように秋月電子通商さんで安価(執筆時点で120円)で入手できるようになった CH32V203K8T6 のソフトウェアを統合開発環境である MounRiver Studio の環境を準備済みなので少し遊んでみました。


1.今回の目標
 目標として部品箱で見つけた少し古いLCDである NOKIA 5110(84x48ドット)を制御してみることにしました。


2.SPI通信
★変更 2024/07/13 全体的に誤記修正(I2C ⇒ SPI)
 CH32V203K8T6 のデータシートは読み易くはないですが、サンプルコードがそれなりにあるので助かります。
 SPIインターフェースを使ってLCDを制御する場合、多くのLCDではコマンドとデータを識別する信号入力があり、この識別信号をSPI通信と同期して切り替える必要があります。このためネット上で見かけるほとんどのコードはSPIの送信レジスタに書き込み後に送信が完了するまで待つようになっています。
 このため連続してSPIで送信する際に送信データ間の隙間が大きくなってしまいます。下図から送信データ間に 11us の隙間が発生していることが判りますが、CPU が 144MHz 動作の割には大きいように感じます。SPIのクロックは 2MHz です。
 因みにネット上をざっくり探しましたがSPIでLCD制御する場合の実測波形は見つかりませんでした。

SPIでの連続した送信例(対処前)


 この送信データ間の隙間時間を短くするために次の対処をやってみました。
  • SPI送信後に送信完了を待たない
  • SPI送信前に送信対象データのコマンド/データの種別が前回の送信と異なる場合はSPI送信完了までウェイトした後にコマンド/データ識別信号を変更する

 変更後のデジアナ波形が下図になります。隙間時間が 11us から 4.5us に短縮されました。

SPIでの連続した送信例(対処後)



3.LCD(NOKIA5110) の制御
 LCDの接続は下表に示す通りでLIGHT信号は100Ωの抵抗を介して接続しました。また、消費電流は 5mA 程度でしたが電源の ON/OFF がし易いように CH32V203 への電源は WCH-LinkE からではなく、安定化電源から給電しています。

CH32V203K8Y6NOKIA 5110WCH-LinkE
1:3.3V6:VCC
5:3.3V6:VCC
7:RA17:LIGHT
8:RA21:RST
9:RA3>2:CE
10:RA43:DC
11:SPI_CK5:CLK
12:SPI_MISO
13:SPI_MOSI4:DIN
16:GND8:GND8:GND
17:3.3V6:VCC
19:TX1:RX
23:SWDIOSWDIO
24:SWCLKSWCLK
32:GND8:GND8:GND


 主な実装機能は下記の通りです。
  1. フォントは「ポケコン(G850)用拡張基板(その7)仮想画面でCP/M」の記事で書いた自作の縮小フォント(4x5)を使用

  2. LCD 内部の RAM の読み出し機能が無いのでアプリ内に VRAM を実装し、テキストとグラフィックの重ね合わせ表示に対応

  3. 画面サイズが 84 x 48 と小さいのでグラフィック機能は凝らずにドット描画、ライン描画、円描画のみに対応


 ライン描画処理は過去にも何回か作成していて今回はケース分けが少なくなるように考えて実装しました。LCDに実際に描画したサンプルが下の写真です。

NOKIA5110での表示例



4.まとめ
 MounRiver Studio を使ってSPIでのLCD(NOKIA 5110)を制御してみました。Arduino 環境であれば割合簡単に動かせるようになると思いますが、Arduino はソースを探す作業がメインになりがちで内部の仕掛けが見えにくいのであまり好きではありません。最初はおそらく10倍以上の手間がかかりますが一度作った機能は次回からは自分ですぐに使えようになるのでその後の作業が楽になるはずです。

★追記 2024/07/14 {
 MounRiver Studio でのディバッグ時の画面キャプチャを貼っておきます。

MounRiver Studio でのディバッグ画面例
}

★追記 2024/07/15 {
 機能追加時に必要となるデータシートから抜粋した CH32V203K8T6 のピンアサイン図と機能対応表を追記します。

ピンアサイン(CH32V203KxT6)


機能対応表(その1)

機能対応表(その2)
}





 まだあまり整理していませんがLCD関連のヘッダと処理コードを貼っておきます。

LCD関連ヘッダファイル
/************************************* LCD drive headder(Nokia 5110) Ver 0.01 2024/06/12 by skyriver *************************************/ #ifndef CONCAT #define TOSTRING(x) #x #define CONCAT(x,y) x##y #define CONCATM(x,y) CONCAT(x,y) #endif #define LCD_MAX_X 84 // max x + 1 #define LCD_MAX_Y 48 // max y + 1 #define LCD_MAX_LN (LCD_MAX_Y / 8 ) #define LCD_CONTRAST 64 // contrast initial value /**** I/O definition ****/ #define LCD_PORT GPIOA // SPI port for LCD #define LCD_SPI SPI1 // SPI number for LCD RA5:SCK,RA6:MISO,RA7:MOSI #define LCD_SPICK GPIO_Pin_5 #define LCD_SPIMISO GPIO_Pin_6 #define LCD_SPIMOSI GPIO_Pin_7 #define LCD_LED GPIO_Pin_1 // LCD LED 0:on #define LCD_RST GPIO_Pin_2 // LCD RST #define LCD_CS GPIO_Pin_3 // SPI_CS #define LCD_CMD GPIO_Pin_4 // LCD CMD/PAR #define LCD_BSET( bits ) GPIO_SetBits( LCD_PORT, bits ) #define LCD_BRESET( bits ) GPIO_ResetBits( LCD_PORT, bits ) #define LCD_SPIWR( dat ) SPI_I2S_SendData( LCD_SPI, dat ) #define LCD_SPIRD() SPI_I2S_ReceiveData( LCD_SPI ) #define LCD_SPIWRSTS() SPI_I2S_GetFlagStatus( LCD_SPI, SPI_I2S_FLAG_TXE ) #define LCD_SPIRDSTS() SPI_I2S_GetFlagStatus( LCD_SPI, SPI_I2S_FLAG_RXNE ) #define LCD_SPIWRBSY() SPI_I2S_GetFlagStatus( LCD_SPI, SPI_I2S_FLAG_BSY ) void LcdCsOn( void ); void LcdCsOff( void ); uint16_t LcdWrCmd( uint8_t dat ); uint16_t LcdWrData( uint8_t dat ); void InitLcd( void ); void LcdLocate( uint8_t x, uint8_t y ); void LcdWrDatas( uint8_t *dpnt, uint16_t len ); void LcdWrFill( uint8_t data, uint16_t len ); void LcdCls( void ); void LcdSetLed( uint8_t led ); void LcdSetCont( uint8_t val ); void LcdPutc( uint8_t data ); void LcdSetPoint( uint8_t x, uint8_t y ); void LcdCircle( uint8_t cx, uint8_t cy, uint8_t r ); void LcdLine( uint8_t xst, uint8_t xen, uint8_t yst, uint8_t yen ); #define FONTSIZX 4 //font width #define FONTSIZY 5 // font hight extern uint8_t Font4x5[]; // font data


LCD関連処理コード
/************************************* LCD driveer(Nokia 5110) Ver 0.02 2024/06/14 by skyriver *************************************/ #include "debug.h" #include <stdint.h> #include "stdlib.h" #include "LcdNokia.h" #define LCDTIMER 1000 // write SPI timer counter typedef enum { DATA, CMD } SpiSortTyp; static SpiSortTyp PrevSort; // spi send previous mode(cmd or data) static uint8_t LcdVram[ LCD_MAX_X * LCD_MAX_LN ]; // LCD VRAM image static uint16_t LcdXPos, LcdYPos; // text write point static uint16_t LcdTxtX, LcdTxtY; // graphic write point // initialize GPIO and SPI void InitLcdSpi( void ) { GPIO_InitTypeDef GPIO_InitStructure={0}; SPI_InitTypeDef SPI_InitStructure={0}; RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE ); // RCC_APB2PeriphClockCmd( CONCATM(RCC_APB2Periph_, LCD_PORT) | RCC_APB2Periph_SPI1, ENABLE ); GPIO_InitStructure.GPIO_Pin = LCD_SPICK | LCD_SPIMOSI; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init( LCD_PORT, &GPIO_InitStructure ); GPIO_InitStructure.GPIO_Pin = LCD_RST | LCD_CS | LCD_CMD | LCD_LED; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init( LCD_PORT, &GPIO_InitStructure ); GPIO_InitStructure.GPIO_Pin = LCD_SPIMISO; // not use GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init( LCD_PORT, &GPIO_InitStructure ); GPIO_SetBits(LCD_PORT, LCD_CS ); GPIO_ResetBits(LCD_PORT, LCD_RST| LCD_LED ); SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx; // SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;//8bit mode0 msbfirst SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 16:500kHz SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init( LCD_SPI, &SPI_InitStructure ); SPI_Cmd( LCD_SPI, ENABLE ); } // set LCD's SPI CS after send complete void LcdCsOn( void ) { while( LCD_SPIWRBSY() == SET ) {} LCD_BRESET( LCD_CS ); } // reset LCD's SPI CS after send complete void LcdCsOff( void ) { while( LCD_SPIWRBSY() == SET ) {} LCD_BSET( LCD_CS ); } // write cmd to LCD // dat <- write data // return -> nonzero:OK, else timeover uint16_t LcdWrCmd( uint8_t dat ) { uint16_t cnt = LCDTIMER; while( --cnt ) { if( LCD_SPIWRSTS() == SET ) { // can write if( PrevSort != CMD ) { PrevSort = CMD; while( LCD_SPIWRBSY() == SET ) {} LCD_BRESET( LCD_CMD ); } LCD_SPIWR( dat ); break; } } if( cnt == 0 ) { printf( "\nLcd timeover" ); } return( cnt ); } // write data to LCD // dat <- write data // return -> nonzero:OK, else timeover uint16_t LcdWrData( uint8_t dat ) { uint16_t cnt = LCDTIMER; while( --cnt ) { if( LCD_SPIWRSTS() == SET ) { // can write if( PrevSort != DATA ) { PrevSort = DATA; while( LCD_SPIWRBSY() == SET ) {} LCD_BSET( LCD_CMD ); } LCD_SPIWR( dat ); break; } } if( cnt == 0 ) { printf( "\nLcd timeover" ); } return( cnt ); } void InitLcd( void ) { InitLcdSpi(); Delay_Ms(100); // LCD_BRESET( LCD_RST ); LCD_BSET( LCD_RST ); LcdCsOn(); LcdWrCmd(0b00100000 | 1 ); // Function set : H = 1 LcdWrCmd(0b00000100 | 1 ); // Temparature control : TC=1(0-3) LcdWrCmd(0b00010000 | 3 ); // Bias system : Bais=3(0-7) LcdWrCmd(0b10000000 | LCD_CONTRAST ); // Set Vop : contrast(0-127) LcdWrCmd(0b00100000 | 0 ); // Function set : H=0 LcdWrCmd(0b00001000 | 4 ); // Display control : normal(D:1,E:0) LcdWrCmd(0b01000000 | 0 ); // Set Y addr : 0(0..5)( x 8 dot) LcdWrCmd(0b10000000 | 0 ); // Set X addr : 0(0..83) LcdCsOff(); } // set write point // x,y <- point addr x:0..83, y:0..5 void LcdLocate( uint8_t x, uint8_t y ) { LcdTxtX = x; LcdTxtY = y; LcdCsOn(); LcdWrCmd(0b01000000 | y ); LcdWrCmd(0b10000000 | x ); LcdCsOff(); } // write datas // dpnt <- data addr // leng <- how many data void LcdWrDatas( uint8_t *dpnt, uint16_t len ) { uint8_t *vpnt = LcdVram + LcdTxtY * LCD_MAX_X + LcdTxtX; LcdTxtX += len; if( LcdTxtX >= LCD_MAX_X ) { LcdTxtY += LcdTxtX / LCD_MAX_X; LcdTxtX = LcdTxtX % LCD_MAX_X; } LcdCsOn(); while( len-- ) { *vpnt |= *dpnt++; LcdWrData( *vpnt++ ); } LcdCsOff(); } // write same data // data <- write data // leng <- how many data void LcdWrFill( uint8_t data, uint16_t len ) { uint8_t *vpnt = LcdVram + LcdTxtY * LCD_MAX_X + LcdTxtX; LcdTxtX += len; if( LcdTxtX >= LCD_MAX_X ) { LcdTxtY += LcdTxtX / LCD_MAX_X; LcdTxtX = LcdTxtX % LCD_MAX_X; } LcdCsOn(); while( len-- ) { LcdWrData( *vpnt++ = data ); } LcdCsOff(); } void LcdCls( void ) { LcdLocate( 0, 0 ); LcdWrFill( 0, LCD_MAX_X * LCD_MAX_LN ); } void LcdSetLed( uint8_t led ) { if( led ) { LCD_BRESET( LCD_LED ); } else { LCD_BSET( LCD_LED ); } } void LcdSetCont( uint8_t val ) { LcdCsOn(); LcdWrCmd(0b10000000 | val ); // contrast(0-127) LcdCsOff(); } // write font data // data <- char code void LcdPutc( uint8_t data ) { LcdWrDatas( &Font4x5[ (data - 0x20) * FONTSIZX ], FONTSIZX ); } // set point // (x,y) <- position void LcdSetPoint( uint8_t x, uint8_t y ) { if( (x < LCD_MAX_X) && (y < LCD_MAX_Y ) ) { uint8_t lin = y >> 3; uint8_t *vpnt = LcdVram + lin * LCD_MAX_X + x; *vpnt |= 1 << (y % 8); LcdLocate( x, lin ); LcdWrDatas( vpnt, 1 ); } } // draw circle void LcdCircle( uint8_t cx, uint8_t cy, uint8_t r ) { uint8_t x = r; uint8_t y = 0; int8_t d = -2 * r + 3; while(x >= y) { LcdSetPoint( cx + x, cy + y ); LcdSetPoint( cx - x, cy + y ); LcdSetPoint( cx + x, cy - y ); LcdSetPoint( cx - x, cy - y ); LcdSetPoint( cx + y, cy + x ); LcdSetPoint( cx - y, cy + x ); LcdSetPoint( cx + y, cy - x ); LcdSetPoint( cx - y, cy - x ); if(d >= 0) { d -= 4 * --x; } y++; d += 4 * y + 2; } } // draw line void LcdLine( uint8_t xst, uint8_t yst, uint8_t xen, uint8_t yen ) { int8_t x,y,difx,dify; int8_t err, xtail, ytail; LcdXPos = xen; LcdYPos = yen; difx = abs( xen - xst ); dify = abs( yen - yst ); if( ( (difx >= dify) && (xen > xst) ) || ( (difx < dify) && (yen > yst) ) ) { x = xst; y = yst; xtail = xen; ytail = yen; } else { x = xen; y = yen; xtail = xst; ytail = yst; } if( difx >= dify ) { int8_t dy; dy = ytail > y ? 1 : -1; err = difx / 2; for( ; x < xtail; x++, xtail-- ) { LcdSetPoint( x, y ); LcdSetPoint( xtail, ytail ); err += dify; if( err >= difx ) { err -= difx; y += dy; ytail -= dy; LcdSetPoint( x, y ); LcdSetPoint( xtail, ytail ); } } if( x == xtail ) { LcdSetPoint( x, y ); } } else { int8_t dx; dx = xtail > x ? 1 : -1; err = dify / 2; for( ; y < ytail; y++, ytail-- ) { LcdSetPoint( x, y ); LcdSetPoint( xtail, ytail ); err += difx; if( err >= dify ) { err -= dify; x += dx; xtail -= dx; LcdSetPoint( x, y ); LcdSetPoint( xtail, ytail ); } } if( y == ytail ) { LcdSetPoint( x, y ); } } }
※Ver0.02 2024/07/14 LcdLine()を両端からの描画方式にして高速化



★追記 2024/07/15
 秋月さんから購入した ILI9341搭載2.8インチTFT液晶 MSP2807(320 x 240 dot)に対応してみました。CH32V203 の内蔵 RAM では AP 内に VRAM 領域を確保できないので文字描画後にグラフィック描画した場合は混在表示できますが、グラフィック描画部に文字を表示した場合は重なった部分のグラフィックは消去されます。

MSP2807 での表示例


 X(旧Twitter)に投稿したメッセージに添付した動画を貼っておきます。本来なら画面クリアしてから描画ONすべきなのですが、書き込み速度が判リ易いように消去処理も表示状態で行っています。




[TOP] [ 前へ ] 連載記事一覧 [ 次へ ]

nice!(0)  コメント(0) 
共通テーマ:趣味・カルチャー

最近遊んだMPU [CH32V]

 最近遊んでみた MPU について書いておきたいと思います。

  1. ルネサス製 RA4M1
     ルネサスさんの Arduino UNO R4 MINIMA を無料で配布し、教育用のビデオで勉強できるという太っ腹企画に応募して最新の UNO R4 を入手出来ました。

    Arduino UNO R4 MINIMA での実験中の様子

     教育用の資料とビデオで Arduino IDE で数個のサンプルプログラムを少しカスタマイズしながら動かしてみました。Arduino は楽でいいですねぇ。
     Arduino IDE は至れり尽くせり感が滲み出てきますがソースを作るより探す面が多くなりがちなことや内部の仕掛けが見えにくい等から個人的にはあまり好きではありません。また RA4M1 は MPU 単体を安価に入手できないのも使用する上で大きなネックになりますね。

    PWM でのLEDジンワリ点滅


  2. WCH製 CH32V
     近年、秋月さんでも取り扱いを始めている安価に入手可能な 32 ビット RISCV MPUです。秋月さんで扱うと電子工作界隈に浸透すると言われますが CH32V はどうでしょうね。私のお気に入りだった PIC24FJ は残念なことにすっかり値段が高くなってしまい使い辛くなってしまいましたが CH32V は円安にも関わらず安価に入手できます。書込みにはWCH-LinkE エミュレータが必要ですが、秋月さんで ¥750 で入手できます。

     最初の設定等は「きょうのかんぱぱ」サイトの「40円RISC-Vマイコン(CH32V003)をArduino IDEでLチカをしてみました」のブログ記事を参考にさせて頂き、すんなり動きました。

    CH32V003J4M6 でのLチカ実験用ハード

     CH32V003J4M6 は安価(¥40)で良いのですが、もっと高機能な CH32V203K8T6(以降CH32V203 と記す)でも遊んでみました。高機能といっても秋月さんで安価(¥120)に入手できます。

     簡単な仕様比較を下表に示します。CH32V203 は SPI 1CH、CAN 1CH、USB 機能等も実装しています。

    clockflashRAMGPIOADCUART/USARTI2CtimerPakage
    CH32V003J4M648MHz16KB2KB6pin6ch1ch1ch4chSOP8
    CH32V203K8T6144MHz64KB20KB26pin10ch2ch1ch6chLQFP32

     CH32V203 は 32 ピンの LQFP パッケージなのでブレッドボードで使えるように DIP 変換基板も購入しました。

    CH32V203 と DIP 変換基板

     ピン間隔が 0.8mm の QFP パッケージなので上下左右の位置合せをしっかるやる必要があり、下図のようにマスキングテープで固定してから半田付けしました。

    CH32V203 の固定

     半田付け後の状態が下の写真になります。うまく半田付けできているみたいですね。

    DIP 変換基板に半田付け後の状態

     Lチカ実験用の回路を下図に示します。ディバッグ用のシリアル受信のために Link-E の Rx も接続しています。

    CH32V203 でのLチカ回路

     ブレッドボードを使って LinkE と LED を接続した状態が下の写真です。右にあるチップは CH32V003J4M6 です。

    CH32V203 でのLチカ実験環境

     Arduino IDE ではすんなりLチカが動きました。

    CH32V203 でのLチカ実験(Arduino IDE)

     次に CH32V の IDE 環境である MounRiver Studio を動かしてみました。
     ”mcuXfamikyのブログ”というブログの「CH32V203(1)資料集め」から始まる一連の記事が大変参考になりました。

     下図が MounRiver Studio でLチカを実行している様子です。ブレークやステップもできるのでこの開発環境は中々いいですね^^

    MounRiver Studio でのLチカ実行画面



★追記 2024/07/15
 「CH32V203で遊ぶ」の記事に MounRiver Studio の開発環境で CH32V203 のソフトウェアを作ってみる内容を記載しました。

nice!(0)  コメント(0) 
共通テーマ:趣味・カルチャー