LoginSignup
0
0

More than 1 year has passed since last update.

Longan Nanoを使ってみる 9 ~A/Dコンバータから入力~

Last updated at Posted at 2022-02-24

前の記事

Longan Nanoを使ってみる 8 ~ボールを動かす~

全体の目次

Longan Nanoを使ってみる 1 ~ビルド環境の構築~
Longan Nanoを使ってみる 2 ~デバッガの環境設定~
 Sipeed RISC-V Debugger
Longan Nanoを使ってみる 3 ~デバッガの使用方法~
Longan Nanoを使ってみる 4 ~printfを使ったデバッグ~
Longan Nanoを使ってみる 5 ~ゲームのプロジェクトを作成~
Longan Nanoを使ってみる 6 ~文字出力~
 Longan Nanoを使ってみる ~FONTX2ファイルを作る~
Longan Nanoを使ってみる 7 ~外枠とブロックを書く~
 Longan Nanoを使ってみる ~謎の画像表示関数~
Longan Nanoを使ってみる 8 ~ボールを動かす~
Longan Nanoを使ってみる 9 ~A/Dコンバータから入力~
Longan Nanoを使ってみる 10 ~パドルを動かす~
Longan Nanoを使ってみる 11 ~ボタンの入力
Longan Nanoを使ってみる 12 ~ボールのロス~
Longan Nanoを使ってみる 13 ~ステージの遷移とゲームオーバー~
Longan Nanoを使ってみる 14 ~PWMとサウンド~
Longan Nanoを使ってみる 15 ~音楽を鳴らす~
Longan Nanoを使ってみる 16 ~とりあえずのまとめ~

はじめに

前の記事では、ほとんど電子工作とは関係ない、Cのプログラムになっていた。この記事では、少し電子工作っぽくなる。
今回は、パドルを動かす準備として、ボリュームをとボタンをLongan Nanoに接続する。

注意

このページは、quiita.com で公開されています。URLがqiita.com以外のサイト、例えばjpdebug.comなどのページでご覧になっている場合、悪質な無許可転載サイトで記事を見ています。正しい記事は、https://qiita.com/BUBUBB/items/7ce85ada67a3f6d1944d からリンクしています。

無許可転載サイトでの権利表記(CC BY SA 2.5、CC BY SA 3.0とCC BY SA 4.0など)は、不当な表示です。
正確な内容は、qiitaのページで参照してください。

用意するもの

これまでの作業で必要だったもの

  • Windows PC (Windows10/11)
  • Longan Nano 本体
  • microSD (16GBytes程度あれば十分すぎる)
  • USB Type-Cケーブル (PCとLongan Nanoを接続する)
  • Sipeed Risk V Debugger
  • 接続ケーブル。Debugger付属、もしくは自作

このページで用意するもの

入力部分を作る

今回、パドルの動作はボリュームで、それ以外にボタンを入力装置として用意する。回路図としては、こんな感じになる。

sym1.png

3V3とGNDを、Longan Nanoから取り、A0ピンとA6ピンにそれぞれアナログ入力とスイッチ入力をつける。青色の線が、スイッチのデジタル入力、緑色の線が、アナログ入力となる。

sch2.png

Longan Nanoに搭載されているGD32Vは、ピン数が100以下なので、VREFは存在せず、3.3V がそのまま基準電圧となる。 そのため、部品点数は3点でよい。

アナログ入力

今回のプログラムでは、可変抵抗で分圧した、0V~3.3Vの電圧を入力して、パドルの操作としている。 この時に GD32Vの、アナログ-デジタルコンバータ(ADC)を使用する。

GD32VのADCは、最大12ビット(0~4096)、2Mサンプル/秒の速度のADCを2チャンネル搭載しており、18アナログ入力チャンネル(うち、外部チャンネルが16、内部温度センサx1、内部リファレンス電圧x1)の入力が可能。

変換には Single conversion mode, Continuous conversion mode , Scan conversion mode , Discontinuous mode と4つのモードがある。

厳密な信号処理では、正確な間隔でサンプリングし、受け取ったデータを遅延なくCPUに流し込まないといけない。そのために、精密なタイマーとDMAを使用する必要があるが、今回は、信号処理ではなく単に人間の入力なので、こうした難しいことはやらず、適当な感じで入力するだけでよい。

初期化処理

最初にADCを初期化する。ここでは、GPIOの初期化を行った後、RCU にADCのクロックのプリスケーラーを設定、ADC0にクロックを供給し、ADC0をオンにする。

ADCの接続を確認すると、ADCは108MHzで駆動されるAPB2に接続されていることがわかる。プリスケーラなどは、108Mhzをベースにしていくことになる。

adc2.png

ADCのプリスケーラーは、マニュアルの5.3.2に解説されている。

adc1.png

ここでわかる通り、2つの場所に分かれた3ビットに値を設定するが、そこはAPIがrcu_adc_clock_configでうまくやってくれる。引数には、RCU_CKADC_CKAPB2_DIV2 (CK_APB2/2から、 DIV2,4,6,8,12,16とそろっている。

Longan Nanoでは、APB2が最大108MHzになっているので、RCU_CKADC_CKAPB2_DIV12を指定すると 9 MhzがADCのプリスケーラに入力されることになる。(ADCは最大14HMz)

paddle.c (最小のコード)

// ボリューム入力の初期化
void Adc_init(void) 
{
	// 	GPIOポートA を、アナログ入力モード、50MHz、ピン0とピン1
    gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0|GPIO_PIN_1);
	// ADCのクロックプリスケーラを APB2/12 に設定する
	rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV12);
	// ADC0にクロックを供給する
    rcu_periph_clock_enable(RCU_ADC0);
	// ADC0 をオンにする
    ADC_CTL1(ADC0)|=ADC_CTL1_ADCON;
}

ADC0をオンにする(ADC_CTL1(ADC0)|=ADC_CTL1_ADCON;) という処理は注意が必要で、マニュアルの11.8.3. C ontrol register 1 では、次のように説明されている。
ADC ON. The ADC will be wake up when this bit is changed from low to high and take a stabilization time. When this bit is high and “1” is written to it with other bits of this register unchanged, the conversion will start.

どうも、このビットは、初期化(The ADC will be wake up when this bit is changed from low to high and take a stabilization time)と変換開始の(When this bit is high and “1” is written to it with other bits of this register unchanged, the conversion will start.)2つの意味を持っているようだ。そのため、adc_iniの中で値を設定し、さらに次のデータ取得処理でも同じ処理を行うことになる。

ただ、今回は、APIを使って書くことを前提にしているので、次のように書き直す。(冗長な設定も含んでいるので、プログラムは長くなっているが、こうすることでどのようなモードで動いているかがわかるのでそのままにしてある。

paddle.h

void Adc_init(void);

paddle.c

// ボリューム入力の初期化
void Adc_init(void) 
{
	// 	GPIOポートA を、アナログ入力モード、50MHz、ピン0とピン1
    gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0|GPIO_PIN_1);
	// ADCのクロックプリスケーラを APB2/12 に設定する
	rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV12);

	// ADC0にクロックを供給する
    rcu_periph_clock_enable(RCU_ADC0);

	// ADCをクリアする
	adc_deinit(ADC0);
	// ADCをフリーモード(全ADCを独立して動作させる)にする。(ADC_CTL0のSYNCMを0b000)
	adc_mode_config(ADC_MODE_FREE);
	// ADCのデータを右詰めにする
	adc_data_alignment_config(ADC0,  ADC_DATAALIGN_RIGHT);
	// チャンネルグループのデータ長を1に設定する。(単発)
	adc_channel_length_config(ADC0,  ADC_REGULAR_CHANNEL,1);
	// ADCの外部トリガーソースを無効にする
 	adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_EXTTRIG_INSERTED_NONE);	
	// ADCの外部トリガーのレギュラーチャンネルを有効にする
	adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE);
	// ADCを有効にする
	adc_enable(ADC0);
    delay_1ms(1);
	// キャリブレーションを行う。この関数は、中でキャリブレーションの完了を待つので、呼び出すだけでよい
    adc_calibration_enable(ADC0);

}

データ取得処理

ADCから値を取得する。今回はプログラムからADCのタイミングを開始し、のんびりと受診すればよい。 変換するチャンネルを指定して、変換開始のビットを立てて、データの読み込みを行い、フラグをクリアする。

Longan Nanoでは、ADCは12ビット精度なので、この関数を呼び出すたびに、ボリュームの位置により、0~4095までの値が戻される。

paddle.h

uint16_t Get_adc(int ch);

paddle.c

//指定されたチャンネルからデータを読みだす
uint16_t Get_adc(int ch)
{
	// 読みだすチャンネルを指定する
	adc_regular_channel_config(ADC0, 0 , (uint8_t)ch, ADC_SAMPLETIME_7POINT5);
	// 読み出しを開始する
	ADC_CTL1(ADC0)|=ADC_CTL1_ADCON;
	// データが出そろうのを待つ
	while(adc_flag_get(ADC0 , ADC_FLAG_EOC) == RESET);
	// データを読みだす
	int ret = adc_regular_data_read(ADC0);
	// フラグをクリアする
	adc_flag_clear(ADC0,ADC_FLAG_EOC); 
    return ret;
}

パドルの入力値として使用する

ADCを使用すると、ボリュームの位置をそのまま取得できる。これをそのままパドルの入力にすると、パドルの動きが敏感すぎたり、左右にプルプル震えたりする。ゲームの入力として使用するには、ADCのデータは「上品さ」に欠けるところがあるようだ。
そこで、ADCの入力をそのままパドルの位置にするのではなく、パドルの速度に上限を持たせる。

  • 値の変化に上限速度を求め、ADCの値が0→1000と瞬間的に動いたとしても、100ずつ10回動くようにする。
  • 0~4096はそのまま使わず、4で割って0~1024までとする。

ADCをそのまま入力値にするのではなく、パドルの位置を内部の変数で持つようにすれば、将来、入力をロータリーエンコーダーにしたときにもこのロジックはそのまま利用できる(かもしれない)。 やることは普通のCのプログラムで、現在の内部値と、ADCの値を比べて、大きい/小さいなら、最大量だけを現在の内部値に足す/引くし、これを出力結果とする。

例えば、ボリュームの位置(=ADCの入力値)が2345の場合、4で割って586を入力値とする。最大移動量が100、現在のパドルの位置が300であれば、移動後のパドルの位置は300 + 100で400とする。 ボリュームの位置が

paddle.h

unsigned int getPaddlePos();

paddle.c

// ADCの値を読み出し、パドルの位置に変更する。
#define MAX_PADDLE_SPEED  100
unsigned int getPaddlePos()
{
	static unsigned int actualValue = -1;

	// 0~4096の値を、4で割って0~1024くらいにしておく。端っこのほうが怪しいから。
	short CurrentPos = Get_adc(0);
	CurrentPos = CurrentPos >> 2;
	if (actualValue == -1) {
		actualValue = CurrentPos;
	 } else {
		int dif = abs(actualValue - CurrentPos);
		if (dif < MAX_PADDLE_SPEED) {
			actualValue = CurrentPos;
		} else {
			if (actualValue > CurrentPos) {
				actualValue-= MAX_PADDLE_SPEED;
			} else {
				actualValue+= MAX_PADDLE_SPEED;
			} 
		}
	 }
	 return actualValue;
}

動作の確認

今回は、メインプログラムにprintfを加え、ADCの動作を確認する。
printfについては、Longan Nanoを使ってみる 4 ~printfを使ったデバッグ~と、プロジェクトの作成 を参照

//
// メイン処理
//
void Game(void) 
{
      :
    LCD_Clear(BLACK);
	Adc_init();
      :
            case STATE_INGAME:{
                :
        		drawDeleteBall(TRUE);			// ひとつ前のボールを消す
				uint16_t val = getPaddlePos();
				printf("VAL:%5d \r\n",val);
                break;                
                :
 

このプログラムをビルド、実行して、printfの結果を参照すると、

VAL: 0
VAL: 0
:
などと表示され、ボリュームを動かすと値が変化する

ここまででできたこと

ADコンバータの設定を行い、ボリュームを操作してその値をプログラムから取得できた

次の記事

Longan Nanoを使ってみる 10 ~パドルを動かす~

今回のソースコード

今回の記事の内容を反映したソースコードは次のようになる。

paddle.h

#ifndef __paddle_h__
#define __paddle_h__
void Adc_init(void) ;
uint16_t Get_adc(int ch);
unsigned int getPaddlePos();
#endif

paddle.c

#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
#include "gd32vf103.h"
#include "game.h"
#include "ball.h"
#include "block.h"
#include "paddle.h"



// ボリューム入力の初期化
void Adc_init(void) 
{
	// 	GPIOポートA を、アナログ入力モード、50MHz、ピン0とピン1
    gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0|GPIO_PIN_1);
	// ADCのクロックプリスケーラを APB2/12 に設定する
	rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV12);

	// ADC0にクロックを供給する
    rcu_periph_clock_enable(RCU_ADC0);

	// ADCをクリアする
	adc_deinit(ADC0);
	// ADCをフリーモード(全ADCを独立して動作させる)にする。(ADC_CTL0のSYNCMを0b000)
	adc_mode_config(ADC_MODE_FREE);
	// ADCのデータを右詰めにする
	adc_data_alignment_config(ADC0,  ADC_DATAALIGN_RIGHT);
	// チャンネルグループのデータ長を1に設定する。(単発)
	adc_channel_length_config(ADC0,  ADC_REGULAR_CHANNEL,1);
	// ADCの外部トリガーソースを無効にする
 	adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_EXTTRIG_INSERTED_NONE);	
	// ADCの外部トリガーのレギュラーチャンネルを有効にする
	adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE);
	// ADCを有効にする
	adc_enable(ADC0);
    delay_1ms(1);
	// キャリブレーションを行う。この関数は、中でキャリブレーションの完了を待つので、呼び出すだけでよい
    adc_calibration_enable(ADC0);

}

game.c

#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
#include "gd32vf103.h"
#include "game.h"
#include "ball.h"
#include "block.h"
#include "paddle.h"

enum GAMESTATE gameState;	    // ゲームの状態
volatile u8 WakeFlag = 0;       // このフラグが1になると、処理が開始される

int LifeCnt = 0;
int Score = 0;

//
// 割り込みハンドラ。タイマーにより指定した周期で非同期に呼び出される
//
void TIMER5_IRQHandler(void)
{
    if(SET == timer_interrupt_flag_get(TIMER5, TIMER_INT_FLAG_UP)){
        timer_interrupt_flag_clear(TIMER5, TIMER_INT_FLAG_UP);
    	WakeFlag = 1;
	    
        static u8 oeFlag;
        if (oeFlag == 0) {
            gpio_bit_reset(GPIOB, GPIO_PIN_8); //OE#
            oeFlag = 1;
        } else {
            gpio_bit_set(GPIOB, GPIO_PIN_8); //OE#
            oeFlag = 0;
        }
    }
}

//
// タイマーの初期化
//
void timer5_config(int Cnt)
{
    // タイマーのパラメータを設定する。
	// タイマーの16ビットプリスケーラには54MHzが入力される・・・はずだが、108MHzが入力されるように振舞っている。
	// それを、10000分周(timer_initpara.prescaler = 10000-1)すると、おおよそ108,000,000/10000= 10.8Khz
    // それを、引数の cnt回数えて(timer_initpara.period = Cnt;)タイマーの周期を決める。
	// 例えば、cntが30の時は、10,800 / 30 =360で、360Hzとなる。。
    timer_parameter_struct timer_initpara;
    rcu_periph_clock_enable(RCU_TIMER5);
    timer_deinit(TIMER5);
    timer_struct_para_init(&timer_initpara);
    timer_initpara.prescaler         = 10000 - 1;
    timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;
    timer_initpara.counterdirection  = TIMER_COUNTER_UP;
    timer_initpara.period            = Cnt;
    timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;
    timer_init(TIMER5, &timer_initpara);
    timer_auto_reload_shadow_enable(TIMER5);
    timer_interrupt_enable(TIMER5, TIMER_INT_UP);

    // 割り込みを有効にして、タイマー5を設定する
    eclic_global_interrupt_enable();
	eclic_set_nlbits(ECLIC_GROUP_LEVEL3_PRIO1);
	eclic_irq_enable(TIMER5_IRQn,1,0);
    
    // タイマーを開始する
    timer_enable(TIMER5);
}



// 外枠とスコア、ライフ残を表示する
void DrawBORDER()
{
	LCD_DrawLine(GAMEAREA_X0,GAMEAREA_Y1,GAMEAREA_X0,GAMEAREA_Y0,WHITE);
	LCD_DrawLine(GAMEAREA_X0,GAMEAREA_Y0,GAMEAREA_X1,GAMEAREA_Y0,WHITE);
	LCD_DrawLine(GAMEAREA_X1,GAMEAREA_Y0,GAMEAREA_X1,GAMEAREA_Y1,WHITE);

	LCD_ShowString(0,0,(const u8 *)"SCORE:",WHITE);
	LCD_ShowString(10,160-12,(const u8 *)"LIFE:",WHITE);
	char life[3];
	sprintf(life,"%1d",LifeCnt);
	LCD_ShowString(55,160-12,(u8 *)life,WHITE);
	u8 scr[12];
	sprintf((char *)scr,"%5d0",Score);
	LCD_ShowString(38,0,scr,WHITE);	
}


// 座標が、矩形の外側に対して、どの象限にいるのかを返す関数
//    1         2        3
//        +---------+
//   4    |    5    |    6
//        +---------+
//   7         8         9    
unsigned char GetOrthant(int x , int y , int x1, int y1 , int x2 , int y2)
{
	bool bLowerX1 =  (x < x1);
	bool bUpperX2 =  (x > x2);
	bool bLowerY1 =  (y < y1);
	bool bUpperY2 =  (y > y2);

    
	if (bLowerX1) {
        return bLowerY1 ? 1: (bUpperY2 ? 7:4);
	} else if (bUpperX2) {
        return bLowerY1 ? 3: (bUpperY2 ? 9:6);
	} else {
        return bLowerY1 ? 2: (bUpperY2 ? 8:5);
	}
}


//
// メイン処理
//
void Game(void) 
{
    rcu_periph_clock_enable(RCU_GPIOB);
    gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_8);	// B8をデバッグに使う

    // 初期化処理
    gameState = STATE_INIT;             // ステータスを初期化にする
    timer5_config(100);                  // タイマーの初期化を行う
    LCD_Init();
    LCD_Clear(BLACK);
	Adc_init();



    u16 tick = 0;                       // LEDを点滅させるためのカウンターを初期化
    gameState = STATE_STARTGAME;        //初期化処理が終了したのでゲーム開始処理を行う
    
    while (TRUE) {
		timer_enable(TIMER5);               // タイマーを有効にする
        // タイマーのウェイト処理。wakeFlagが割り込みルーチン内で1になるまで無限ループする
		
        while(WakeFlag == 0) {
            delay_1ms(10);
            break;
        }

        tick = (tick + 1) & 0x8FFF;     // LEDの点滅用カウンタのインクリメント
		WakeFlag = 0;                   // タイマーのウエイトフラグを初期化する
        
        switch (gameState) {
            case STATE_STARTGAME:{
                /*ゲームの開始処理 */
                Score = 0;
                LifeCnt = 3;
                DrawBORDER();               //外枠とライフ残、スコアを画面に表示させる
                InitBlock();
                DrawBlock();
                InitBallPos(0,NULL);
                gameState = STATE_INGAME;
                break;
            }
            case STATE_INGAME:{
        		drawDeleteBall(FALSE);			// ひとつ前のボールを消す
	            u8 ret= moveBall();
	            if (ret == 0) {

                }
        		drawDeleteBall(TRUE);			// ひとつ前のボールを消す
				uint16_t val = getPaddlePos();
				//char buf[100];
				printf("VAL:%5d \r\n",val);
				//LCD_ShowString(0,10,buf,RED);
				
                break;                
            }
            case STATE_GAMEOVER:{
                /*ゲーム-オーバー処理*/
                break;
            }
        }

    }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0