Arduinoでクルマのエンジンが動く？　オープンソースECU Speeduino (7)ソースコードの構造


#Speeduinoのソースコードを読み解く
前回記事   [(6)燃料噴射量・点火時期制御](https://qiita.com/covao/items/60f7adf1cc0fcdf31880)

Speeduinoの制御を理解するため、ソースコードを解読していきます。
ソースコードを理解することで、いろいろとカスタマイズすることもできます。:smiling_imp:
##GitHubのソースコード
- Speeduinoコミュニティの中で日々更新されており、移植性やコード品質の向上が行われているようです。
- そのため、流動的であり、あるコードが別のソースファイルの関数へ移動することもあります。:cry:
- ここでは、[2018.10リリースのソースコード](https://github.com/noisymime/speeduino/releases/tag/201810
)について記載しています。

#ソースコードファイル一覧
以下のようなソースコードファイルから構成されています。
srcフォルダ内に複数のライブラリがありますが、一部しか利用していないようです。

|ソース|ヘッダー|概要|
|:--------------------|:--------------------|:------------------|
|[auxiliaries.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/auxiliaries.ino)|[auxiliaries.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/auxiliaries.h)|ファン制御、ブースト制御など|
|[cancomms.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/cancomms.ino)|[cancomms.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/cancomms.h)|CAN通信用マイコンとのシリアル通信処理|
|[comms.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/comms.ino)|[comms.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/comms.h)|TunerStudioとのシリアル通信コマンド処理|
|[corrections.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/corrections.ino)|[corrections.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/corrections.h)|様々な制御処理の集合|
|[crankMaths.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/crankMaths.ino)|[crankMaths.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/crankMaths.h)|クランク角算出処理|
|[decoders.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/decoders.ino)|[decoders.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/decoders.h)|クランク角パルス信号処理|
|[display.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/display.ino)|[display.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/display.h)|QLEDディスプレイ（SSD1306)表示（オプション）|
|[errors.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/errors.ino)|[errors.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/errors.h)|故障診断エラーコード処理（故障診断は未実装）|
||[globals.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/globals.h)|グローバル変数定義|
|[idle.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/idle.ino)|[idle.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/idle.h)|アイドル回転数制御|
|[maths.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/maths.ino)|[maths.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/maths.h)|固定小数点演算ライブラリ|
|[scheduledIO.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/scheduledIO.ino)|[scheduledIO.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/scheduledIO.h)|点火処理スケジュール|
|[scheduler.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/scheduler.ino)|[scheduler.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/scheduler.h)|タイマー割込み定義、クランク角センサ割込み定義|
|[sensors.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/sensors.ino)|[sensors.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/sensors.h)|センサーA/D値読込み処理|
|[speeduino.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/speeduino.ino)|[speeduino.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/speeduino.h)|初期化処理と様々なバックグラウンド処理|
|[storage.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/storage.ino)|[storage.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/storage.h)|EEPROMデータ処理|
|[table.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/table.ino)|[table.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/table.h)|ルックアップテーブル関数|
|[timers.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/timers.ino)|[timers.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/timers.h)|タイマー割込み処理|
|[updates.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/updates.ino)|[updates.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/updates.h)|EEPROMアップデート処理(初期化時)|
|[utils.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/utils.ino)|[utils.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/utils.h)|マイコン依存処理定義（I/Oピン、割込み)、クランク角パルス処理選択|
|[src/PID_v1/PID_v1.cpp](https://github.com/noisymime/speeduino/blob/201810/speeduino/src/PID_v1/PID_v1.cpp)|[src/PID_v1/PID_v1.h](https://github.com/noisymime/speeduino/blob/201810/speeduino/src/PID_v1/PID_v1.h)|PID制御ライブラリ|


##コンパイルスイッチ
- 複数のターゲットマイコンでコードを共有化するため、#if definedマクロ等が多くあります。:hushed:
- 対象のマイコン以外ではコンパイルされないため、ソースコードを読むときに注意が必要です。:confounded:

リリース 2018.10では、Arduino Mega以外は、まだ実装途中のようです。:open_mouth:

|#defineマクロ|マイコンボード|
|:--------|:----------|
|\_\_AVR_ATmega1280\_\_|Arduino Mega1280|
|\_\_AVR_ATmega2560\_\_|Arduino Mega2560|
|CORE_TEENSY|Teensy|
|CORE_STM32|STM32|
|ARDUINO_ARCH_STM32|STM32 GENERIC core|

**コンパイルスイッチの例**

```ruby:utils.ino
uint16_t freeRam ()
{
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
  extern int __heap_start, *__brkval;
  uint16_t v;

  return (uint16_t) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);

#elif defined(CORE_TEENSY)
  uint32_t stackTop;
  uint32_t heapTop;

  // current position of the stack.
  stackTop = (uint32_t) &stackTop;

  // current position of heap.
  void* hTop = malloc(1);
  heapTop = (uint32_t) hTop;
  free(hTop);

  // The difference is the free, available ram.
  return (uint16_t)stackTop - heapTop;
#elif defined(CORE_STM32)
  char top = 't';
  return &top - reinterpret_cast<char*>(sbrk(0));

#endif
}
```
[GitHubソースコード] (https://github.com/noisymime/speeduino/blob/201810/speeduino/utils.ino#L17)


##初期化関数 [setup()] (https://github.com/noisymime/speeduino/blob/201810/speeduino/speeduino.ino#L119)
マイコン起動時の初期化処理を記述しています。

**主要なコール関数**

|関数|処理|
|:--------|:----------|
|[initialiseTimers()](https://github.com/noisymime/speeduino/blob/201810/speeduino/timers.ino#L24)|タイマー割込み(1ms)の設定|
|[loadConfig()](https://github.com/noisymime/speeduino/blob/201810/speeduino/storage.ino#L393)| EEPROMのパラメータ読込み|
|[doUpdates()](https://github.com/noisymime/speeduino/blob/201810/speeduino/updates.ino#L11)|EEPROMのアップデート|

```ruby:speeduino.ino
void setup()
{
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
  //Setup the dummy fuel and ignition tables
  //dummyFuelTable(&fuelTable);
  //dummyIgnitionTable(&ignitionTable);
  table3D_setSize(&fuelTable, 16);
  table3D_setSize(&ignitionTable, 16);
  table3D_setSize(&afrTable, 16);
  table3D_setSize(&stagingTable, 8);
  table3D_setSize(&boostTable, 8);
  table3D_setSize(&vvtTable, 8);
  table3D_setSize(&trim1Table, 6);
  table3D_setSize(&trim2Table, 6);
  table3D_setSize(&trim3Table, 6);
  table3D_setSize(&trim4Table, 6);
  initialiseTimers();

  loadConfig();
  doUpdates(); //Check if any data items need updating (Occurs ith firmware updates)

```


###ループ処理関数(バックグランド処理) [loop()](https://github.com/noisymime/speeduino/blob/201810/speeduino/speeduino.ino#L859)


- タイマー割込み([timers.ino] (https://github.com/noisymime/speeduino/blob/201810/speeduino/timers.ino))でセットされるTIMER_maskを参照し、周期毎の処理を行っています。
- その他、ループ回数毎に行う処理も存在します。

**主要なコール関数**

|周期|関数|処理|
|:--------|:--------|:--------|
|31loop|[command()](https://github.com/noisymime/speeduino/blob/201810/speeduino/comms.ino#L21)|シリアル通信コマンド処理|
|31loop|[canCommand()](https://github.com/noisymime/speeduino/blob/201810/speeduino/cancomms.ino#L19)|CAN通信用シリアルコマンド処理|
|66ms|[readTPS()](https://github.com/noisymime/speeduino/blob/201810/speeduino/sensors.ino#L258)|TPS読込み|
|33ms|[boostControl()](https://github.com/noisymime/speeduino/blob/201810/speeduino/auxiliaries.ino#L126)|ブースト制御|
|250ms|[readO2()](https://github.com/noisymime/speeduino/blob/201810/speeduino/sensors.ino#L340)|O2センサ読込み|
|250ms|[readBat()](https://github.com/noisymime/speeduino/blob/201810/speeduino/sensors.ino#L368)|バッテリ電圧読込み|
|250ms|[nitrousControl()](https://github.com/noisymime/speeduino/blob/201810/speeduino/auxiliaries.ino#L232)|NOx制御|
|250ms|[vvtControl()](https://github.com/noisymime/speeduino/blob/201810/speeduino/auxiliaries.ino#L202)|VVT制御|
|250ms|[idleControl()](https://github.com/noisymime/speeduino/blob/201810/speeduino/idle.ino#L218)|アイドル回転数制御|
|1000ms|[readBaro()](https://github.com/noisymime/speeduino/blob/201810/speeduino/sensors.ino#L321)|大気圧センサ読込み|
|1loop|[correctionsFuel()](https://github.com/noisymime/speeduino/blob/201810/speeduino/corrections.ino#L39)|燃料噴射量算出|
|1loop|[getVE()](https://github.com/noisymime/speeduino/blob/201810/speeduino/speeduino.ino#L2038)|VE算出|
|1loop|[getAdvance()](https://github.com/noisymime/speeduino/blob/201810/speeduino/speeduino.ino#L2062)|点火時期算出|
|1loop|[PW()](https://github.com/noisymime/speeduino/blob/201810/speeduino/speeduino.ino#L1998)|インジェクタパルス幅算出|


```ruby:speeduino.ino
void loop()
{
      mainLoopCount++;
      LOOP_TIMER = TIMER_mask;
      //Check for any requets from serial. Serial operations are checked under 2 scenarios:
      // 1) Every 64 loops (64 Is more than fast enough for TunerStudio). This function is equivalent to ((loopCount % 64) == 1) but is considerably faster due to not using the mod or division operations
      // 2) If the amount of data in the serial buffer is greater than a set threhold (See globals.h). This is to avoid serial buffer overflow when large amounts of data is being sent
      //if ( (BIT_CHECK(TIMER_mask, BIT_TIMER_15HZ)) || (Serial.available() > SERIAL_BUFFER_THRESHOLD) )
      if ( ((mainLoopCount & 31) == 1) or (Serial.available() > SERIAL_BUFFER_THRESHOLD) )
      {
        if (Serial.available() > 0) { command(); }
        else if(cmdPending == true)
        {
          //This is a special case just for the tooth and composite loggers
          if (currentCommand == 'T') { command(); }
        }
        
      }
```

###クランク角センサ割込み
- クランク角センサパルスをトリガーに起動されます。
- 割込み処理により。パルスの周期を計測し、回転数、クランク角を算出しています。
- 様々な車種のクランク角センサ信号処理関数があらかじめ用意されています。

|関数|処理|
|:--------|:--------|
|triggerSetup_xxx()|クランク角センサパラメータ定義|
|triggerPri_xxxx()|1stクラック角センサ信号処理|
|triggerSec_xxxx()|2ndクラック角センサ信号処理|
|getRPM_xxxx()|回転数算出|
|getCrankAngle_xxxx()|クランク角算出|

[decoders.ino](https://github.com/noisymime/speeduino/blob/201810/speeduino/scheduler.ino)

```ruby:decoders.ino
/*
Speeduino - Simple engine management for the Arduino Mega 2560 platform
Copyright (C) Josh Stewart
A full copy of the license may be found in the projects root directory
*/

/*
This file contains the various crank and cam wheel decoder functions.

Each decoder must have the following 4 functions (Where xxxx is the decoder name):

* triggerSetup_xxx - Called once from within setup() and configures any required variables
* triggerPri_xxxx - Called each time the primary (No. 1) crank/cam signal is triggered (Called as an interrupt, so variables must be declared volatile)
* triggerSec_xxxx - Called each time the secondary (No. 2) crank/cam signal is triggered (Called as an interrupt, so variables must be declared volatile)
* getRPM_xxxx - Returns the current RPM, as calculated by the decoder
* getCrankAngle_xxxx - Returns the current crank angle, as calculated b the decoder

And each decoder must utlise at least the following variables:
toothLastToothTime - The time (In uS) that the last primary tooth was 'seen'
*

*/
```

##1msタイマー割込み [OneMSInterval()](https://github.com/noisymime/speeduino/blob/201810/speeduino/decoders.ino)
・1msのタイマー割込み処理で、一定周期ごと(33ms,66ms,100ms,250ms等)、TIMER_maskフラグをセットしています。
・周期的な処理の多くは、このフラグを基にバックグラウンド処理(loop関数)の中で行われています。

```ruby:timers.ino
//Timer2 Overflow Interrupt Vector, called when the timer overflows.
//Executes every ~1ms.
#if defined(CORE_AVR) //AVR chips use the ISR for this
ISR(TIMER2_OVF_vect, ISR_NOBLOCK) //This MUST be no block. Turning NO_BLOCK off messes with timing accuracy
#elif defined (CORE_TEENSY) || defined(CORE_STM32)
void oneMSInterval() //Most ARM chips can simply call a function
#endif
{
  ms_counter++;

  //Increment Loop Counters
  loop33ms++;
  loop66ms++;
  loop100ms++;
  loop250ms++;
  loopSec++;

  unsigned long targetOverdwellTime;

  //Overdwell check
  targetOverdwellTime = micros() - dwellLimit_uS; //Set a target time in the past that all coil charging must have begun after. If the coil charge began before this time, it's been running too long
  bool isCrankLocked = configPage4.ignCranklock && (currentStatus.RPM < currentStatus.crankRPM); //Dwell limiter is disabled during cranking on setups using the locked cranking timing. WE HAVE to do the RPM check here as relying on the engine cranking bit can be potentially too slow in updating
  //Check first whether each spark output is currently on. Only check it's dwell time if it is

  if(ignitionSchedule1.Status == RUNNING) { if( (ignitionSchedule1.startTime < targetOverdwellTime) && (configPage4.useDwellLim) && (isCrankLocked != true) ) { endCoil1Charge(); ignitionSchedule1.Status = OFF; } }
  if(ignitionSchedule2.Status == RUNNING) { if( (ignitionSchedule2.startTime < targetOverdwellTime) && (configPage4.useDwellLim) && (isCrankLocked != true) ) { endCoil2Charge(); ignitionSchedule2.Status = OFF; } }
  if(ignitionSchedule3.Status == RUNNING) { if( (ignitionSchedule3.startTime < targetOverdwellTime) && (configPage4.useDwellLim) && (isCrankLocked != true) ) { endCoil3Charge(); ignitionSchedule3.Status = OFF; } }
  if(ignitionSchedule4.Status == RUNNING) { if( (ignitionSchedule4.startTime < targetOverdwellTime) && (configPage4.useDwellLim) && (isCrankLocked != true) ) { endCoil4Charge(); ignitionSchedule4.Status = OFF; } }
  if(ignitionSchedule5.Status == RUNNING) { if( (ignitionSchedule5.startTime < targetOverdwellTime) && (configPage4.useDwellLim) && (isCrankLocked != true) ) { endCoil5Charge(); ignitionSchedule5.Status = OFF; } }



  //30Hz loop
  if (loop33ms == 33)
  {
    loop33ms = 0;
    BIT_SET(TIMER_mask, BIT_TIMER_30HZ);
  }

  //15Hz loop
  if (loop66ms == 66)
  {
    loop66ms = 0;
    BIT_SET(TIMER_mask, BIT_TIMER_15HZ);
  }

  //Loop executed every 100ms loop
  //Anything inside this if statement will run every 100ms.
  if (loop100ms == 100)
  {
    loop100ms = 0; //Reset counter
    BIT_SET(TIMER_mask, BIT_TIMER_10HZ);

    currentStatus.rpmDOT = (currentStatus.RPM - lastRPM_100ms) * 10; //This is the RPM per second that the engine has accelerated/decelleratedin the last loop
    lastRPM_100ms = currentStatus.RPM; //Record the current RPM for next calc
  }

  //Loop executed every 250ms loop (1ms x 250 = 250ms)
  //Anything inside this if statement will run every 250ms.
  if (loop250ms == 250)
  {
    loop250ms = 0; //Reset Counter
    BIT_SET(TIMER_mask, BIT_TIMER_4HZ);
    #if defined(CORE_STM32) //debug purpose, only visal for running code
      digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
    #endif

    #if defined(CORE_AVR)
      //Reset watchdog timer (Not active currently)
      //wdt_reset();
      //DIY watchdog
      //This is a sign of a crash:
      //if( (initialisationComplete == true) && (last250msLoopCount == mainLoopCount) ) { setup(); }
      //else { last250msLoopCount = mainLoopCount; }
    #endif
  }
```

##参考
[Speeduino Wiki コードの概要](https://speeduino.com/wiki/index.php/Code_overview)
##最後に
[次回](https://qiita.com/covao/items/70ebf54c70fd86357a66)は、ソースコードで使われているデータを読み解く予定です。