16
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

第一回RSP杯チキンレース 参戦レポート

Last updated at Posted at 2020-01-09

趣味で宇宙開発を行う団体「リーマンサット・プロジェクト(Ryman Sat Project=rsp.)」がお送りする新春アドベントカレンダーです。
4日目の記事です。インデックス記事はこちら

こんにちは。超小型人工衛星「RSP-01」の自撮り機能を担当している ひろゆき(@Gan0803)です。

リーマンサット・プロジェクトでは、技術レベル向上を目的とした様々な活動が行われており、その一つがFormula Chicken Association(リーマンサットの内部組織)の主催するRSP杯チキンレースです。

少し前ですがこの大会に参加するため、ミニ四駆に「PICマイコン」「加速度センサ」「測距センサ」「モータードライバ」「定電圧回路」を組み込んでレースマシンを作ったときに学んだことが、マイコン・電子工作初級者のステップアップにぴったりだと思ったので振り返って共有します。

このレースマシン製作では、センサから入力を得て、マイコンで車体状態を推定し、モータードライバに出力して車体を制御する、というマイコンを使った制御の基本を学ぶことができました。また、モジュールを組み込むための回路設計とはんだ付けの練習にもなりました。そして、ハードウェアとソフトウェアを統合して一つのシステムを作り上げる難しさと楽しさを実感しました。

これが製作したマシンです。コンパクトにまとめることができたと思います。
レースマシン

壁に近づくとこんな感じで止まります。画像をクリックするとyoutubeで動画を確認できます。
動画

この記事では、レースマシンを開発する際に自身がステップアップできたと思う内容を中心に書いていきます。

目次

大会概要

レース概要は以下のとおりです。

  • タミヤ社製ミニ四駆を用いてチキンレースを行う。
  • 10m手前から壁に向かって走り始め、出来るだけ壁に近い距離で止まったミニ四駆が勝利。壁に当たった場合は失格。
  • 制限時間はスタートから10秒間とする。10秒経過時点で車体が停止されていなければならない。
  • スタート時点では、ミニ四駆はマイコンの電源を完全に切った停止状態とする。
  • 2台同時出走の勝ち抜き戦。
  • 個人でもチームでも参加OK。
  • 他車の妨害はしないこと。
  • 1mm以上移動すること。

また、車体規定には以下のものがあります。

  • サイズ、重量不問。但し、車体の移動はミニ四駆搭載のモーターのみによって行う。
  • マイコンとセンサをそれぞれ最低1種類は搭載し、ミニ四駆を制御すること。マイコンも、Arduino, Raspberry Pi, IchigoJam, PIC, mbed……など種類は問いません。
  • 出走後の外部からのコントロールは不可。

※楽しく技術を向上させることが大会の目的であるため、規則は変更および柔軟に運用されることがあります。

開発の方針

  • 好きなサイクロンマグナムをベースに格好良く仕上げたい。
    • サイクロンマグナムのボディ内にすべてを収めたい。制御基板を45mm x 30mm x 20mmに収める必要がある。
  • 前から気になっていたPICを使ってみたい。
    • 必要最低限の機能のチップに詰め込むチャレンジをする。
  • 精度はそんなに高くなくていい。
  • あんまりお金はかけないようにする。

構成部品

いろんな部品があって迷いましたが、自分で選ぶことでどんな部品があるか勉強になりました。秋月、千石、マルツなどに何度も通うことになり、電子部品のお店にも少しだけ詳しくなりました。失敗して部品を壊すことが多いので、安いものは予備を含めて2~3個買っておくと何度も買いに行く手間が省けます(笑)。

部品一覧

部品は以下のものを選びました。
**部品にリチウムイオンポリマー電池を使用していますが、液もれ、発熱、破裂、発火の危険があるため取り扱いに注意が必要です。代替可能な手段がある場合はそちらを使用したほうが良いです。**専用のリチウムイオン電池充電器で充電し、保管時は防火性のケースに入れています(防火性の記載があっても燃えるものもあるので注意……)。使用時の出力も計算して設定しています。しかし、いま振り返るとリチウム一次電池のほうが良いと思います。

部品 品名 購入価格 備考
シャシー サイクロンマグナム 687円 好きなので!
マイコン PIC12F1822 110円 選択理由は後述
ソケット 8ピンソケット 100円 書き込みのために抜き差しするので必要
距離センサ シャープ 測距モジュール GP2Y0E02A 740円 素直にレーザー測距モジュール買えばよかったかも
加速度センサ MMA8452Q 350円 最低限の3軸センサ
モータードライバ DRV8835 300円 お気に入り
定電圧モジュール 低損失三端子レギュレーター 3.3V 1A TA48033S 100円
電池(マイコン用) リチウムイオンポリマー電池 40mAh 500円くらい? 取り扱い注意!
電池(モーター用) リチウムイオンポリマー電池 280mAh 500円くらい? 取り扱い注意!
ユニバーサル基板 AB-J11TH(43.18mm × 30.48mm) 115円 車体に収まるギリギリサイズ
抵抗、コンデンサ、ダイオード、LED 中国製の安い詰め合わせをAmazonで購入 100円(必要な個数だけなら)
ケーブル AWG20 耐熱ケーブル 530円
コネクタ SHコネクタXHコネクタ 100円くらい
スズメッキ線 スズメッキ線 210円
スイッチ 小型スライドスイッチ 25円
デバッグ用LCD I2C接続小型キャラクタLCDモジュール 8×2行 320円 ブレッドボードで使用
合計 4787円 安くはない?

センサの選定

センサ

チキンレースでは壁との距離を測る必要があるので距離センサが必要です。主な距離センサとして以下のものがあります。

  • 超音波センサ:安価で測定距離も長いのですが、サイズが大きめ。
  • レーザー測距モジュール:測定距離が長く小型ですが少しお高いです。
  • 赤外線測距モジュール:測定距離は短めだが小型のものもあり比較的安価です。

超音波センサは車体に収まらず、レーザー測距モジュールは予算オーバーのため、赤外線測距モジュールでがんばります。
赤外線測距モジュールは4cm~50cmの距離しか計測できないため、フルスピードを出しているとセンサの範囲内に入ってからの減速が間に合いません。そのため速度を調整する必要があります。そこで距離センサの範囲内に入るまでは加速度センサを使って速度と距離を測定し、車体を制御する作戦にしました。
加速度センサは進行方向だけ観測できればいいので一番安い3軸のものにしました。

センサはアナログ、I2C、SPIなどで値を取るものがあります。今回選んだ距離センサはアナログ接続で、加速度センサはI2C接続です。**I2Cは2本という少ない信号線に最大112個のデバイスを接続することができ、速度も100kbps、400kbps、1Mbpsと比較的高速でプログラム上も難しくなく、とても使いやすいです。**I2CはSPIよりも扱いやすかったです(個人の感想です)。ブレッドボード上での検証を進める途中でシリアル出力用のピンを確保できなくなったため、I2C接続のLCDを追加しましたが容易に追加できました。

PICの選定

PIC

PICは安価で手に入れやすく、ラインアップが豊富なマイコンです。DIPと表面実装どちらも選べます。

PICについては必要な機能とピン数(I/O)から選びます。今回は、モーターの制御用にPWM、加速度センサにI2C、距離センサにADCが必要です。モータードライバ用とセンサ用に必要なピンの合計は5本です。最低限なら8ピンのPICでいけそうですね。

PICの選定は MICROCHIP ADVANCED PART SELECTOR を使いました。
必要な機能を以下の通り入れていくと候補が出てきます。

  • CPU Type: 8-bit PIC MCU
  • ADC Input: 2(0より大きい最低値)
  • UART: 1(デバッグ用)
  • I2C: 1
  • Max PWM Outputs: 1
    MAPS.png
    今回は最低限の機能に挑戦するため、候補リストの一番下に位置するPIC12F1822を選択します。PIC12F1840だと少し楽になりますが、自分を追い込むため制限の厳しい方でギリギリを狙います。
    今回の開発過程で知ったことですが、通常の開発では最高性能でピン数の多いものを選び、開発の後工程でピン配置に互換性がある必要十分なグレードのチップに切り替えるようです。そのほうがリソース不足で設計の見直しが発生する可能性も低く、開発作業も楽になります。

PICKit3

構成部品ではないですが、PICへのプログラム書き込みにはライターが必要です。今回はPICKit3を使います。正規品で5000円、Amazonで売っている互換品で2000円くらいです。ライターはちょっと高いですが、チップが安いのでたくさんモノを作るうちにもとが取れる、はず。

開発の準備

PICで動作するプログラムの開発にはMPLAB-X IDEを使用します。詳細なセットアップ手順は検索するとたくさん出てくるのでここでは最低限だけ記載して折りたたんでおきます。興味のある方は後で確認してみてください。

MPLAB X IDEのインストールとプロジェクトの作成

MPLAB X IDEのインストール

MPLAB X IDEのサイトから最新版をダウンロードしてインストールします(以下の図ではMPLAB X IDE v5.30)。
MPLAB_X_IDE_DL

MPLAB XC8 Compilerもダウンロードしてインストールします(以下の図ではMPLAB XC8 Compiler v2.10、インストール時のLicense TypeはFreeを選択)。
PLAB_XC8_COMPILER

プロジェクトの作成

  • Choose Project
    • Categories: Microchip Embedded
    • Projects: Standalone project
    • 使用するDeviceを選択します。ここではPIC12F1822です。
  • Select Device
    • Family: Mid-Range 8-bit MCUs (PIC10/12/16/MCP)
    • Device: PIC12F1822
  • Supported Debug Header: None
    • 今回は使用しません。
  • Select Tool (Optional):
    • PICKit3を選択します。PICKit3をPCに接続しているとシリアル番号が表示されるので選択します。
  • Select Compiler: XC8(v2.10)
    • 先ほどダウンロードしてインストールしたXC Compilerを選択します。
  • Select Project Name and Folder:
    • 任意の名前をつけてください。
    • WindwosならEncodingはShift JISにしておきましょう。

これでプロジェクトはできました。ソースファイルを追加してプログラミングを始めることができます。

MCC(MPLAB Code Configurator)

MPLAB X IDEを使っていて一番便利だったのがMCCというプラグインです。デバイスのコンフィグレーション(使いたい機能、周波数、割り込みの設定)をGUIで設定すると、必要なコードを自動生成してくれます。

Tools > Plugin > Available Pluginsタブから、MPLAB Code Configuratorをインストールします。
インストールすると下図の通りアイコンが出てくるのでクリックします。
MCC.png

MCCの画面に切り替わるので、Device Resourcesから使用したい機能を選んでProject Resourcesに追加すると、PinManagerの画面に対応する機能の行が追加されます。
今回はADC(アナログ・デジタル変換)、MSSP(I2C)、ECCP(PWM)、TMR1とTMR2(タイマー割り込み)を使用するためProject ResourceのPeripheralsに追加しています。
MPLAB_MCC.png
使いたい機能を追加したら、Pin Managerでピンに機能を割り当てていきます。ピンによって割り当てられる機能が異なるため、Pin Managerのピンと機能のマトリックスで割当がぶつからないように選択します。今回は図の通り無駄なく割り当てることができました(緑の鍵がかかっているところ)。ここはパズルみたいで楽しかったです。

また、これらの機能を利用したプログラムを書く場合、サンプルコードが自動生成されたヘッダの中に記載されているので持ってきていじるだけです。
例えば、I2Cのコードの雛形はMCCで自動生成されたHeader Files/MCC Generated Files/i2c.hの中にコメントとして記載されています。

PICへのプログラム書き込み回路の準備

作成したプログラムを書き込むためには、PICKit3とPICを接続する必要があります。
PICKit3とPIC12F1822の同じ名前のピン同士をつなぐだけです。

PICKit3のピン配置は以下のようになっています([http://ww1.microchip.com/downloads/jp/DeviceDoc/52010A_JP.pdf])
PICKit3_pin.png

PIC12F1822のピン配置もデータシートで確認すると以下のようになっています。
PIC12F1822_pin.png

書き込み中はMCLRをVILに保持する必要があります。MCLRとVDDを10kΩのプルアップ抵抗で接続すればいいようです。

データシートにはそのモジュールの使い方を含めた全ての情報が記載されているため、一次情報として必ずデータシートを確認します。

回路の設計と実装

ここからは実際にPICやセンサなどのモジュールを使って回路を作ります。

ブレッドボードモデル

人工衛星の開発でも同じですが、まずはブレッドボード上で単体のモジュールを動作確認しながら使い方を確認していきます。いきなり複数のモジュールを組み合わせると問題が起きたときに原因の特定が難しいため、まず個別に確認を行います。
ここでは例として、加速度センサMMA8452Qの動作を確認する場合について記載します。モジュールの説明書とデータシートを確認しながらプログラムを作成します。

説明書を参考にブレッドボード上でセンサとPICを接続します。ここではPIC書き込み用の回路も一つのブレッドボード上に同居しています。
MMA8452Q動作確認

データシートの情報通りに加速度センサとI2Cで通信するプログラムを作成します。ここで作成したプログラムでは1秒おきにMMA8452Qから加速度を取得し、シリアルで出力します(PCとUSBシリアルで接続して確認します)。このプログラムを実行しながらブレッドボードを傾けると出力される加速度の値が変わります。

加速度センサの動作確認プログラム(クリックして展開)
#include "mcc_generated_files/mcc.h"
#include <stdlib.h>

#define MMA8452_ADRS 0x1D    
#define MMA8452_STATUS 0x00
#define MMA8452_XYZ_DATA_CFG 0x0E
#define MMA8452_CTRL_REG1 0x2A
#define MMA8452_CTRL_REG1_ACTV_BIT 0x01
#define MMA8452_WHO_AM_I 0x0D
#define MMA8452_CTRL_REG1_CONFIG 0x00
#define MMA8452_G_SCALE 2
#define SLAVE_I2C_GENERIC_RETRY_MAX 10

uint8_t g_buf[7] = {0, 0, 0, 0, 0, 0, 0};

void writeI2C(uint8_t addr, uint8_t* writeData, uint8_t len) {
    I2C_MESSAGE_STATUS status;
    I2C_MasterWrite(writeData, len, addr, &status);
    while (status == I2C_MESSAGE_PENDING){
        __delay_us(100);
    }
}

void readI2C(uint8_t addr, uint8_t reg, uint8_t* readData, uint8_t len) {
    
    I2C_MESSAGE_STATUS status;
    I2C_TRANSACTION_REQUEST_BLOCK readTRB[2];
    uint16_t    timeOut;

    // this initial value is important
    status = I2C_MESSAGE_PENDING;

    // we need to create the TRBs for a random read sequence to the EEPROM
    // Build TRB for sending address
    I2C_MasterWriteTRBBuild(   &readTRB[0],
                                    &reg,
                                    1,
                                    addr);
    // Build TRB for receiving data
    I2C_MasterReadTRBBuild(    &readTRB[1],
                                    readData,
                                    len,
                                    addr);

    timeOut = 0;

    while(status != I2C_MESSAGE_FAIL)
    {
        // now send the transactions
        I2C_MasterTRBInsert(2, readTRB, &status);

        // wait for the message to be sent or status has changed.
        while(status == I2C_MESSAGE_PENDING){
            __delay_us(100);
        }

        if (status == I2C_MESSAGE_COMPLETE)
            break;

        // if status is  I2C_MESSAGE_ADDRESS_NO_ACK,
        //               or I2C_DATA_NO_ACK,
        // The device may be busy and needs more time for the last
        // write so we can retry writing the data, this is why we
        // use a while loop here

        // check for max retry and skip this byte
        if (timeOut == SLAVE_I2C_GENERIC_RETRY_MAX)
            break;
        else
            timeOut++;
    }
}

void printSerial(uint8_t* data, uint8_t len) {
    for(int i = 0; i < len; i++) {
        EUSART_Write(data[i]);
    }
}

void initMMA8452Q() {
    uint8_t len = 1;
    I2C_MESSAGE_STATUS status;

    readI2C(MMA8452_ADRS, MMA8452_WHO_AM_I, &g_buf, 1);
    printSerial(g_buf, 1);
    
    g_buf[1] = 0x00;
    g_buf[0] = MMA8452_CTRL_REG1;
    writeI2C(MMA8452_ADRS, g_buf, 2);
    printSerial(g_buf, 2);

    g_buf[0] = MMA8452_XYZ_DATA_CFG;
    g_buf[1] = (MMA8452_G_SCALE >> 2);
    writeI2C(MMA8452_ADRS, g_buf, 2);
    printSerial(g_buf, 2);

    readI2C(MMA8452_ADRS, MMA8452_CTRL_REG1, &g_buf, 1);
    printSerial(g_buf, 1);
    
    g_buf[1] = MMA8452_CTRL_REG1_CONFIG | MMA8452_CTRL_REG1_ACTV_BIT;
    g_buf[0] = MMA8452_CTRL_REG1;
    writeI2C(MMA8452_ADRS, g_buf, 2);
}

void main(void)
{
    // initialize the device
    SYSTEM_Initialize();

    // When using interrupts, you need to set the Global and Peripheral Interrupt Enable bits
    // Use the following macros to:

    // Enable the Global Interrupts
    INTERRUPT_GlobalInterruptEnable();

    // Enable the Peripheral Interrupts
    INTERRUPT_PeripheralInterruptEnable();

    // Disable the Global Interrupts
    //INTERRUPT_GlobalInterruptDisable();

    // Disable the Peripheral Interrupts
    //INTERRUPT_PeripheralInterruptDisable();

    __delay_ms(1000);
    
    initMMA8452Q();
    
    readI2C(MMA8452_ADRS, MMA8452_STATUS, &g_buf, 7);
    
    while (1)
    {
        readI2C(MMA8452_ADRS, MMA8452_STATUS, &g_buf, 7);
        printSerial(g_buf, 6);

        __delay_ms(1000);
    }
}

I2Cの処理は生成されたヘッダのコメント部分から持ってきています。

モジュール単体での確認が終わったら、モジュールを結合しての確認を行います。
下の写真では、一通りの部品(PIC12F1822、加速度センサ、距離センサ、モータードライバ、モーター、バッテリー、定電圧回路、LCD)をブレッドボード上で接続した状態です。
ブレッドボード上で組み合わせ

PICで加速度センサの値を読み取ってY軸とZ軸の値をLCDに表示しています(動かしていないので0に近い値になっています)。同時に距離センサで計測した距離に応じてモーターの回転を制御しています。

ブレッドボード上ではモジュールをPICの対応するピンへ単純に接続していくだけで、大きな問題もなく動かすことができました。

基板実装

設計した回路をユニバーサル基板上に実装していきます。

まず、ブレッドボード上で確認した構成をもとに回路図を作成します。回路図の作成にはFritzingを使用しました。回路図と合わせて基板上での配置も確認できるため便利です。

基板に実装してからも少し手直しが発生しましたが回路図は以下のようになりました。
回路図
モーターの駆動でバッテリー電圧が下がってPICがリセットすると嫌だなと思ったのでモーター用と制御用の電源を分けています。通電確認用にLEDを入れたり、各モジュールのデータシートに従って抵抗やコンデンサを入れています。ダイオードは電流の逆流が心配だったので入れてみました。LCDはミニ四駆に組み込むときには接続しません。

この回路をユニバーサル基板上に配置するのですが、サイズに制限があるためどのように組み込むか苦労しました。Frizting上で作ってはやり直しを3回ほど繰り返して下の写真のような配置にしました。正直、このユニバーサル基板によく詰め込んだと思います(笑)。
基板に実装

しかしこのサイズでは空中配線(裏側)も仕方なし……。もっと小さくきれいに実装するために、次はプリント基板に挑戦したくなりました。はんだづけも精進が必要ですね(リーマンサットでははんだ付け講習会も開催しています、参加したい……)。
配線したらテスターで通電チェックし、意図通りのところが接続しているかと意図しないところが接触していないかをしっかり確認します。
確認したにもかかわらず、意図しない配線が接触しておりMMA8452Qを2個壊しました……。

ミニ四駆の電池を入れる部分をくり抜いてこの基盤を収めます。距離センサ、スイッチ、バッテリーはコネクタで接続できるようにして、シャシーにボンドで接着しています。距離センサ前面はボディのウィンドウの部分をくり抜いて前が見えるようにしています。これでようやくハードウェアが形になりました。
基板の固定はシャシーにプラネジをボンドで付けて、ネジで止めるようにしました。車重は105g(バッテリー込み)、ノーマルモーターを使用しています。
ミニ四駆に取り付け

何度もシャシーに基板をつけたり取り外したりしているうちに配線がちぎれてしまうことがありました……。しっかりはんだ付けした上でボンドで配線の根元を固めるとちぎれにくくなります。

基板実装後にI2Cの通信がうまくいっているかわからない状態になったので、ロジックアナライザを導入しました。問題を物理的な配線か、ソフトウェアの問題化を切り分けられるので、I2Cなどの通信のデバッグにはロジックアナライザがあると便利です(オシロスコープもあると良い)。
使ったのは1000円程度の低価格ロジックアナライザですが、ちゃんとI2Cの信号を見ることができました。

制御ソフトウェアの開発

ハードウェアが固まってきたことでようやく制御ソフトウェアの開発ができます。
PIC12F1822のプログラムメモリは2KB、SRAMは128バイトです。動作周波数は最大32MHzですが、今回は8MHzで動かすことにします。この限られたリソースの中にプログラムを詰め込まなければならないと考えると、ワクワクしてきますね。

今回作成したプログラムでは、加速度センサと距離センサから読み取った計測値をもとにマシンを制御する処理を1秒間に50回実行しています。
PICのプログラムでは無限ループの中に処理を記述していきます。ループの処理はタイマー割り込みによって制御され20msに一度実行されます。1周の処理にかかる時間は20ms弱で、センサからの値を取得する時間が大きな割合を占めます。

距離センサの範囲に入るまでは、加速度センサの値から進んだ距離と速度を計算してマシンをコントロールしています。最初は加速して2m/sくらいまで速度を上げて距離を稼ぎ、測距センサの有効距離である壁の50cm手前までに40cm/s以下に減速させておけば、距離センサの計測距離に入ってからのブレーキが間に合うという考えです。

問題は加速度センサの誤差がどれくらい出るかです。速度と距離は加速度センサの値を積分して求めるため、センサの誤差が増幅されて蓄積していきます。
そこで、加速度センサだけを使って指定の距離で停止させるテストをしてみたところ、加速度センサの値から計算した距離と実際の距離のズレはプラスマイナス10%程度でした。もっと誤差が大きくなるんじゃないかと思ってましたがこれならなんとか使えそうです。50cm+誤差を含めた距離で減速位置を調整します。

一定速度で距離センサの計測範囲内に入れるようになったら、距離センサで速度と距離を計測して停止位置の調整をします。
距離センサのデータシートを見ると計測値が安定するまでに40ms必要なようです。常時計測しながら走っても遅延が出そうですが、処理のループが1周20msですしあまり気にしないことにします。
距離はアナログ電圧で取得しており、ADCの分解能では0.1mmくらいが限界です。また、測距センサの赤外線を反射する対象の素材や環境によって計測値がぶれます。0.5mmくらいは誤差がありそうです。
ここでは、距離センサの計測範囲内に入ってから速度を10cm/s以下まで落とし、壁の直前で完全停止するようにしました。しかし、全体の処理が50Hzなのでそのまま計測値を使うと速度の誤差が25cm/sになるため、速度を10cm/sに調整しようとしてもマイナス2.5cm/s~22.5cm/sになってしまいます。なので、誤差の影響を減らすためにcurrent = current * 0.2 + last * 0.8として過去の値から少しずつ変化させるようにしてみました。

そして、実験で速度やブレーキに適した位置を調べておきます。速度についてはPWMと比例するので、事前に確認しておけばだいたいいい感じになります。今回はフローリングで確認しましたが路面によっても状況が変わります。レース当日はタイルカーペットでした……。

恥を忍んでコードも載せておきます。

レースマシンのプログラム(クリックして展開)

本記事を書く上で室内で動作確認していたので距離が3.5mに合わせてあります。

#include "mcc_generated_files/mcc.h"
#include <stdint.h>

/*
                         Main application
 */

//#define LCD_ADDR 0x3E
#define MMA8452_ADRS 0x1D    
//#define MMA8452_STATUS 0x00
#define MMA8452_OUT_Y_MSB 0x03
#define MMA8452_XYZ_DATA_CFG 0x0E
#define MMA8452_CTRL_REG1 0x2A
#define MMA8452_CTRL_REG1_ACTV_BIT 0x01
//#define MMA8452_WHO_AM_I 0x0D
#define MMA8452_CTRL_REG1_CONFIG 0x00
#define MMA8452_G_SCALE 2
#define SLAVE_I2C_GENERIC_RETRY_MAX 10

uint8_t g_buf[2] = {0, 0};
bool g_flag_exec = false;

void writeI2C(uint8_t addr, uint8_t* writeData, uint8_t len) {
    I2C_MESSAGE_STATUS status;
    I2C_MasterWrite(writeData, len, addr, &status);
    while (status == I2C_MESSAGE_PENDING){
        __delay_us(100);
    }
}

void readI2C(uint8_t addr, uint8_t reg, uint8_t* readData, uint8_t len) {
    
    I2C_MESSAGE_STATUS status;
    I2C_TRANSACTION_REQUEST_BLOCK readTRB[2];
    uint16_t    timeOut;

    // this initial value is important
    status = I2C_MESSAGE_PENDING;

    // we need to create the TRBs for a random read sequence to the EEPROM
    // Build TRB for sending address
    I2C_MasterWriteTRBBuild(   &readTRB[0],
                                    &reg,
                                    1,
                                    addr);
    // Build TRB for receiving data
    I2C_MasterReadTRBBuild(    &readTRB[1],
                                    readData,
                                    len,
                                    addr);

    timeOut = 0;

    while(status != I2C_MESSAGE_FAIL)
    {
        // now send the transactions
        I2C_MasterTRBInsert(2, readTRB, &status);

        // wait for the message to be sent or status has changed.
        while(status == I2C_MESSAGE_PENDING){
            __delay_us(100);
        }

        if (status == I2C_MESSAGE_COMPLETE)
            break;

        // if status is  I2C_MESSAGE_ADDRESS_NO_ACK,
        //               or I2C_DATA_NO_ACK,
        // The device may be busy and needs more time for the last
        // write so we can retry writing the data, this is why we
        // use a while loop here

        // check for max retry and skip this byte
        if (timeOut == SLAVE_I2C_GENERIC_RETRY_MAX)
            break;
        else
            timeOut++;
    }
}



void initMMA8452Q() {
    
    g_buf[1] = 0x00;//g_buf[0] & ~(MMA8452_CTRL_REG1_ACTV_BIT);
    g_buf[0] = MMA8452_CTRL_REG1;
    writeI2C(MMA8452_ADRS, g_buf, 2);

    g_buf[0] = MMA8452_XYZ_DATA_CFG;
    g_buf[1] = (MMA8452_G_SCALE >> 2);
    writeI2C(MMA8452_ADRS, g_buf, 2);
    
    readI2C(MMA8452_ADRS, MMA8452_CTRL_REG1, &g_buf, 1);
    
    g_buf[1] = MMA8452_CTRL_REG1_CONFIG | MMA8452_CTRL_REG1_ACTV_BIT;
    g_buf[0] = MMA8452_CTRL_REG1;
    writeI2C(MMA8452_ADRS, g_buf, 2);
}

int getRange() {
    uint16_t result = 0;
    result = ADC_GetConversion(channel_AN0);
    return result;
}

int getAccel() {
    int16_t result = 0;

    readI2C(MMA8452_ADRS, MMA8452_OUT_Y_MSB, &g_buf, 2);

    result = (g_buf[0] << 4) + (g_buf[1] >> 4);
    if(result >= 0x0800) {
        result -= 4096;
    }
    
    return result;
}

void timer1Callback(void) {
    g_flag_exec = true;
}

/*
                         Main application
 */
void main(void)
{
    // initialize the device
    SYSTEM_Initialize();

    TMR1_SetInterruptHandler(timer1Callback);

    // When using interrupts, you need to set the Global and Peripheral Interrupt Enable bits
    // Use the following macros to:

    // Enable the Global Interrupts
    INTERRUPT_GlobalInterruptEnable();

    // Enable the Peripheral Interrupts
    INTERRUPT_PeripheralInterruptEnable();

    // Disable the Global Interrupts
    //INTERRUPT_GlobalInterruptDisable();

    // Disable the Peripheral Interrupts
    //INTERRUPT_PeripheralInterruptDisable();

    uint16_t range = 0;
    uint16_t last_range = 0;
    int32_t accel_offset = 0;
    int32_t accel = 0;
    uint16_t duty = 0;
    uint8_t brake_phase = 0;
    int32_t velocity = 0;
    int32_t last_velocity = 0;
    bool g_flag_finish = false;
    uint16_t phase = 0;
    
    RA4 = 0;

    initMMA8452Q();

    __delay_ms(800);
        
    for(int i=0; i < 16; i++) {
        accel_offset += getAccel();
        __delay_us(1250);
    }
    accel_offset >>= 4;
    
    int32_t trip = 0;
    
    while (1)
    {
        duty = 0;
        
        if(g_flag_exec && !g_flag_finish) {
            
            if(phase == 0) {
                range = getRange();
                
                accel = 0;
        
                // 加速度は加速方向がマイナス、減速方向がプラス
                for(int i = 0; i < 4; i++){
                    accel += (getAccel() - accel_offset); // milli G, 1G = 9800mm/s^2
                    __delay_us(1250);
                }
                accel >>= 2;

                velocity += (accel * -191); // (-accel * 9.8m/s^2 / 1024) * 20ms unit is um/s
                if(velocity < 0) {
                    velocity = 0;
                }
                trip += ((velocity + last_velocity) / 100); // (current + last) / 2 um/s * 20ms / 1000 sec unit is um (10m = 10,000,000 um))
            
                if(trip <= 2000000) {
                    duty = 200;
                } else if(2500000 < trip) {
                    duty = 40;
                }

                if(250 < range && range < 600) {
                    phase = 1;
                }
            } else {
                range = 0;
                for(int i = 0; i < 8; i++){
                   range += getRange();
                    __delay_us(2000);
                }
                range = range >> 3;
        
                range = (range * 2 + last_range * 8) / 10;
                velocity = (range - last_range) * 50; // mm/s 20ms周期のループなので50mm/s単位でしかわからない。
                
                if(range < 250) { // 50cm
                    RA4 = 0;
                    duty = 30;
                    
                    if (brake_phase == 1) {
                        brake_phase = 2;
                        RA4 = 0;
                        duty = 50;
                    }
                }else {
                    RA4 = 0;
                    duty = 20;
                    
                    if(brake_phase < 2 && velocity > 50) {
                        RA4 = 1;
                        duty = 200;
                        brake_phase = 1;
                    }

                    if(range > 450) { // 30cm
                        RA4 = 1;
                        duty = 100;
                        g_flag_finish = true;
                    }
                }
            }
            
            EPWM_LoadDutyValue(duty);
            
            last_velocity = velocity;
            last_range = range;
            g_flag_exec = false;
        } else if(g_flag_finish) {
            __delay_ms(100);
            RA4 = 1;
            duty = 0;
            EPWM_LoadDutyValue(duty);
        }
    } 
}

これをビルドするともういっぱいいっぱいです。

Memory Summary:
    Program space        used   7E4h (  2020) of   800h words   ( 98.6%)
    Data space           used    77h (   119) of    80h bytes   ( 93.0%)
    EEPROM space         used     0h (     0) of   100h bytes   (  0.0%)
    Data stack space     used     0h (     0) of     7h bytes   (  0.0%)
    Configuration bits   used     2h (     2) of     2h words   (100.0%)
    ID Location space    used     0h (     0) of     4h bytes   (  0.0%)

ちなみに、このプログラムはちょっとチキンになりすぎることもありました(笑)。
super_chiken_30.gif

今回、ハードウェアの製作に時間が取られたためソフトウェアに時間を割くことができませんでした。美しいとはお世辞にも言い難い、いきあたりばったりの制御になっています。本当はフィードバック制御についてちゃんと勉強して、マシンの数理モデルを作ってみたかったです。
また、加速度センサの誤差が許容範囲だったとはいえ、加速度センサから求められる速度や距離は誤差が大きいため、タイヤの回転数を取得するロータリーエンコーダなどを併用すればよかったなと思いました。

レース結果

3位表彰台を獲得することができました。
タイルカーペットでシャシーこすりながらうねうね走るのを見守っているときはヒヤヒヤしました。途中コンセントの出っ張りを踏んで直角に曲がっていったりもしました。なんとなくダートでのレースになるとは感づいてはいましたが、悪路に対応した設計が必要でした。
レース中に衝突事故を起こす車が1台もなかったのは幸いです。

まとめ

レースマシン製作を通じてPICマイコンの使い方を覚えたことで工作の幅が広がりました(人工衛星の制御ソフトの開発でも役に立っています)。プリント基板と表面実装チップを使えばさらなる小型化も可能だと思います。また、回路設計をしたことがなくても必要な部品をデータシートどおりに接続すれば動く回路が作れるんだな、と回路に対するハードルが下がった気がします。

以下のことも学びました。

  • リチウムイオンポリマー電池は絶対必要でない限り代替手段を使うほうが安全で良い。
  • I2Cは2本の信号線に最大112個のデバイスを接続することができ、比較的高速でとても使いやすい。
  • 開発中は高性能のマイコンを使い、組み込み時にピン互換性のある下位のマイコンに切り替える。
  • MPLAB X IDE、特にMCCが便利。
  • データシートは必ず読むべし。
  • 回路もソフトと同じくブレッドボード上の単体試験から行う。
  • 配線したらテスターで通電チェック(身にしみた)。
  • ボンドで配線の根元を固めるとちぎれにくくなる。
  • I2Cなどの通信のデバッグにはロジックアナライザがあると便利。
  • センサの精度は確認する。加速度センサから求められる速度や距離は誤差が大きい。

今回、モチベーションを維持しながら学習できたのは他の参加者がいたことや、自分の進捗・学びを聞いてくれる人がいた点が大きかったです。学習する際にはぜひ友人や仲間などと一緒にやることをおすすめします。リーマンサット・プロジェクトに入って一緒に技術向上するのも楽しいと思います。

リーマンサット・プロジェクトは「普通の人が集まって宇宙開発しよう」を合言葉に活動をしている民間団体です。
他では経験できない「宇宙開発プロジェクト」に誰もが携わることができます。
興味を持たれた方は https://www.rymansat.com/join からお気軽にどうぞ。

次回は@yohachiさんの「開発環境の1つであるVSCodeの開発環境についてチラ見してみる」です。

関連文書

16
2
2

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
16
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?