C言語で始める組み込みシステム開発入門:基礎から応用まで

組み込みシステムは、私たちの日常生活に欠かせないさまざまな機器の中核をなす技術です。スマートフォンや家電、自動車など、多くのデバイスが組み込みシステムによって動作しています。本記事では、C言語を使った組み込みシステム開発の基本を学びます。初心者にもわかりやすいように、基本概念から具体的なプログラム例、さらには応用例まで幅広くカバーします。

目次

組み込みシステムとは

組み込みシステムは、特定の機能を実現するために設計された専用のコンピュータシステムです。これらは、家電製品、自動車、医療機器など、日常的に使用される多くのデバイスに組み込まれています。組み込みシステムは、リアルタイムで動作し、効率的かつ信頼性の高いパフォーマンスを提供する必要があります。

組み込みシステムの特徴

組み込みシステムは以下の特徴を持ちます:

  • 特定のタスクに最適化されている
  • リアルタイム性が求められる
  • リソースが限られている(メモリ、CPUパワー)
  • 高い信頼性と安定性が必要

組み込みシステムの応用例

組み込みシステムは多くの分野で応用されています。例えば:

  • 家電製品(洗濯機、冷蔵庫)
  • 自動車(エンジン制御ユニット、エアバッグシステム)
  • 医療機器(心拍モニター、インスリンポンプ)

組み込みシステムの重要性

組み込みシステムは、日常生活の多くの側面において不可欠です。これらのシステムは、効率性、安全性、快適性を向上させるために設計されており、技術の進歩に伴い、その重要性はますます高まっています。

C言語の基本構文

組み込みシステム開発において、C言語は非常に重要な役割を果たします。その理由は、C言語がハードウェアに近いレベルで動作するため、高いパフォーマンスと効率を提供できるからです。ここでは、C言語の基本構文を学び、組み込みシステム開発の基礎を築きます。

変数とデータ型

C言語でプログラムを作成するためには、まず変数とデータ型を理解する必要があります。変数はデータを保存するための名前付きの場所であり、データ型は変数に保存されるデータの種類を定義します。主なデータ型には以下のものがあります:

  • int: 整数
  • float: 浮動小数点数
  • char: 文字
int a = 10;
float b = 5.5;
char c = 'A';

制御構造

C言語には、プログラムの流れを制御するための構造がいくつかあります。これには条件分岐やループが含まれます。

条件分岐

条件分岐は、特定の条件に基づいて異なるコードブロックを実行するために使用されます。主要な構文には、if文、else文、switch文があります。

int num = 5;
if (num > 0) {
    printf("Positive number\n");
} else {
    printf("Non-positive number\n");
}

ループ

ループは、特定のコードブロックを繰り返し実行するために使用されます。主なループ構造には、forループ、whileループ、do-whileループがあります。

for (int i = 0; i < 10; i++) {
    printf("%d\n", i);
}

関数

関数は、特定のタスクを実行するためのコードブロックを定義します。関数を使用することで、コードの再利用性と可読性を向上させることができます。

int add(int x, int y) {
    return x + y;
}

int result = add(3, 4);
printf("Result: %d\n", result);

これらの基本構文を理解することで、C言語でのプログラム作成の基礎が築かれます。次に、これらの知識を組み込みシステム開発に応用するための方法を学びます。

開発環境の構築

組み込みシステム開発を始めるためには、適切な開発環境を構築することが重要です。ここでは、C言語での組み込み開発に必要なツールと環境の設定方法を説明します。

必要なツール

組み込みシステム開発には、以下のツールが必要です:

  • コンパイラ:C言語のコードをコンパイルするためのツール。代表的なものにGCC(GNU Compiler Collection)があります。
  • IDE(統合開発環境):コードを書くためのエディタやデバッグツールを統合した環境。代表的なものにEclipse、Visual Studio Codeがあります。
  • デバッガ:プログラムをデバッグするためのツール。GDB(GNU Debugger)などが一般的です。

開発環境のセットアップ手順

ここでは、GCCとVisual Studio Codeを使用した開発環境のセットアップ手順を説明します。

ステップ1:GCCのインストール

まず、GCCをインストールします。WindowsではMinGW、macOSではHomebrew、Linuxではパッケージマネージャを使用してインストールできます。

Windows(MinGW)の例:

pacman -S mingw-w64-x86_64-gcc

macOS(Homebrew)の例:

brew install gcc

Linux(Ubuntu)の例:

sudo apt-get install gcc

ステップ2:Visual Studio Codeのインストール

次に、Visual Studio Codeをインストールします。公式サイトからダウンロードしてインストールします。

[h3]ステップ3:拡張機能のインストール[/h3]
Visual Studio CodeにC/C++拡張機能をインストールします。これにより、コード補完やデバッグ機能が利用可能になります。

  1. Visual Studio Codeを開き、左側の「拡張機能」アイコンをクリックします。
  2. 検索バーに「C/C++」と入力し、Microsoftが提供する拡張機能をインストールします。

ステップ4:デバッガの設定

GDBを使用してデバッグを行うための設定を行います。Visual Studio Codeの設定ファイル(launch.json)に以下のように記述します。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "GDB",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/a.out",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": true,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "build",
            "miDebuggerPath": "/usr/bin/gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "logging": {
                "engineLogging": true
            }
        }
    ]
}

開発環境のテスト

環境が正しく構築されたかを確認するために、簡単なCプログラムを作成してコンパイル・実行してみます。

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

以上で、C言語を使用した組み込みシステム開発のための環境構築が完了です。次に、マイクロコントローラーの基礎知識について学びます。

マイコンの基礎知識

マイクロコントローラー(マイコン)は、組み込みシステムの中核をなす重要なコンポーネントです。ここでは、マイコンの基本的な仕組みと使用方法について解説します。

マイクロコントローラーとは

マイクロコントローラーは、単一の集積回路(IC)に中央処理装置(CPU)、メモリ、および入出力(I/O)ポートを統合したコンピュータです。一般的には、以下の要素が含まれています:

  • CPU:プログラムの命令を実行する中央処理装置
  • メモリ:プログラムとデータを保存する場所。RAM(揮発性メモリ)とROM(不揮発性メモリ)
  • I/Oポート:外部デバイスとの通信を行うためのポート

マイクロコントローラーのアーキテクチャ

マイコンのアーキテクチャには、ハーバードアーキテクチャとノイマンアーキテクチャの2種類があります。

ハーバードアーキテクチャ

プログラムメモリとデータメモリが分離しているアーキテクチャ。高速なメモリアクセスが可能で、組み込みシステムによく使われます。

ノイマンアーキテクチャ

プログラムメモリとデータメモリが統合されているアーキテクチャ。汎用性が高く、一般的なコンピュータシステムで広く使われています。

代表的なマイクロコントローラー

組み込みシステム開発でよく使用されるマイクロコントローラーには以下のものがあります:

  • AVRマイクロコントローラー(Atmel社)
  • PICマイクロコントローラー(Microchip社)
  • ARM Cortexシリーズ(ARM社)

マイクロコントローラーのプログラミング

マイコンのプログラミングは、通常C言語やアセンブリ言語を使用して行います。以下は、一般的なマイコンプログラムの構造です。

#include <avr/io.h>

int main(void) {
    // 初期化
    DDRB |= (1 << DDB5); // ポートBの5番ピンを出力に設定

    while (1) {
        // メインループ
        PORTB ^= (1 << PORTB5); // LEDを点滅
        _delay_ms(1000); // 1秒待機
    }

    return 0;
}

開発ツールとデバッグ

マイコンのプログラム開発には、専用の開発ツールとデバッガが必要です。以下は、一般的なツールの例です:

  • 開発環境:Atmel Studio、MPLAB X
  • プログラム書き込みツール:AVRISP、PICkit
  • デバッガ:JTAG、ICE(In-Circuit Emulator)

これらの知識を基に、次はC言語を用いた基本的な入出力操作の方法について学びます。

基本的な入出力操作

組み込みシステムでは、マイクロコントローラーと外部デバイスとの間でデータのやり取りを行うことが重要です。ここでは、C言語を用いた基本的な入出力操作の方法を学びます。

デジタル入力と出力

デジタル入力と出力は、マイクロコントローラーの基本的な機能の一つです。これにより、ボタンの状態を読み取ったり、LEDを点灯させたりすることができます。

デジタル出力

デジタル出力を使用してLEDを点灯させる例です。

#include <avr/io.h>

int main(void) {
    // ポートBの5番ピンを出力に設定
    DDRB |= (1 << DDB5);

    while (1) {
        // ポートBの5番ピンをHIGHに設定(LED点灯)
        PORTB |= (1 << PORTB5);

        // 1秒待機
        _delay_ms(1000);

        // ポートBの5番ピンをLOWに設定(LED消灯)
        PORTB &= ~(1 << PORTB5);

        // 1秒待機
        _delay_ms(1000);
    }

    return 0;
}

デジタル入力

デジタル入力を使用してボタンの状態を読み取る例です。

#include <avr/io.h>

int main(void) {
    // ポートBの0番ピンを入力に設定
    DDRB &= ~(1 << DDB0);

    // 内部プルアップ抵抗を有効にする
    PORTB |= (1 << PORTB0);

    while (1) {
        // ポートBの0番ピンの状態を読み取る
        if (PINB & (1 << PINB0)) {
            // ボタンが押されていない場合の処理
        } else {
            // ボタンが押された場合の処理
        }
    }

    return 0;
}

アナログ入力

アナログ入力は、センサーからの信号を読み取るために使用されます。以下は、アナログ入力を使用して温度センサーのデータを読み取る例です。

#include <avr/io.h>

void adc_init(void) {
    // ADCを初期化
    ADMUX = (1 << REFS0); // 参照電圧をAVccに設定
    ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); // ADC有効化、プリスケーラ128
}

uint16_t adc_read(uint8_t ch) {
    // チャンネル選択
    ch &= 0b00000111; // チャンネルは0〜7
    ADMUX = (ADMUX & 0xF8) | ch;

    // 変換開始
    ADCSRA |= (1 << ADSC);

    // 変換完了待機
    while (ADCSRA & (1 << ADSC));

    return (ADC);
}

int main(void) {
    adc_init(); // ADC初期化

    while (1) {
        uint16_t adc_result = adc_read(0); // チャンネル0から読み取り
        // adc_resultを処理
    }

    return 0;
}

入出力の応用例

以下に、デジタル入力と出力を組み合わせた簡単な応用例を示します。ボタンを押すとLEDが点灯します。

#include <avr/io.h>
#include <util/delay.h>

int main(void) {
    // 入力と出力の設定
    DDRB &= ~(1 << DDB0); // ボタンピンを入力に設定
    PORTB |= (1 << PORTB0); // 内部プルアップ抵抗を有効にする
    DDRB |= (1 << DDB5); // LEDピンを出力に設定

    while (1) {
        // ボタンの状態をチェック
        if (!(PINB & (1 << PINB0))) {
            // ボタンが押された場合、LEDを点灯
            PORTB |= (1 << PORTB5);
        } else {
            // ボタンが押されていない場合、LEDを消灯
            PORTB &= ~(1 << PORTB5);
        }
    }

    return 0;
}

このように、基本的な入出力操作を習得することで、さまざまなデバイスとのインターフェースを作成することができます。次に、タイマーと割り込み処理の基礎について学びます。

タイマーと割り込み処理

タイマーと割り込みは、組み込みシステムにおいて重要な役割を果たします。これらを理解することで、効率的なプログラムを作成し、リアルタイムでの処理を実現できます。ここでは、タイマーと割り込み処理の基礎を学び、実際にプログラムします。

タイマーの基本

タイマーは、一定の時間間隔でイベントを発生させるためのハードウェア機能です。これにより、時間管理や周期的な処理を行うことができます。

タイマーの設定例

以下は、AVRマイクロコントローラーでのタイマーの設定例です。タイマー0を使用して1秒ごとにLEDを点滅させます。

#include <avr/io.h>
#include <avr/interrupt.h>

void timer0_init(void) {
    // タイマー0の初期化
    TCCR0A = (1 << WGM01); // CTCモード
    TCCR0B = (1 << CS02) | (1 << CS00); // プリスケーラ1024
    OCR0A = 156; // コンペアマッチの値を設定(16MHzクロックの場合、1秒)
    TIMSK0 = (1 << OCIE0A); // コンペアマッチ割り込みを有効化
}

ISR(TIMER0_COMPA_vect) {
    // 割り込みハンドラ:LEDの点滅
    PORTB ^= (1 << PORTB5);
}

int main(void) {
    // ポートBの5番ピンを出力に設定
    DDRB |= (1 << DDB5);
    // タイマー0の初期化
    timer0_init();
    // グローバル割り込みを有効化
    sei();

    while (1) {
        // メインループ
    }

    return 0;
}

割り込みの基本

割り込みは、特定のイベントが発生した際に、メインプログラムの実行を一時停止して、割り込みハンドラ(ISR:Interrupt Service Routine)を実行する機能です。これにより、迅速な応答が可能になります。

外部割り込みの設定例

以下は、AVRマイクロコントローラーでの外部割り込みの設定例です。ボタンを押すと割り込みが発生し、LEDが点滅します。

#include <avr/io.h>
#include <avr/interrupt.h>

void external_interrupt_init(void) {
    // 外部割り込みの初期化
    EICRA = (1 << ISC01); // フォーリングエッジで割り込み
    EIMSK = (1 << INT0); // INT0を有効化
}

ISR(INT0_vect) {
    // 割り込みハンドラ:LEDの点滅
    PORTB ^= (1 << PORTB5);
}

int main(void) {
    // ポートBの5番ピンを出力に設定
    DDRB |= (1 << DDB5);
    // 外部割り込みの初期化
    external_interrupt_init();
    // グローバル割り込みを有効化
    sei();

    while (1) {
        // メインループ
    }

    return 0;
}

タイマーと割り込みの応用例

以下に、タイマーと外部割り込みを組み合わせた応用例を示します。ボタンを押すと、1秒ごとにLEDが点滅します。

#include <avr/io.h>
#include <avr/interrupt.h>

volatile uint8_t led_state = 0;

void timer0_init(void) {
    // タイマー0の初期化
    TCCR0A = (1 << WGM01); // CTCモード
    TCCR0B = (1 << CS02) | (1 << CS00); // プリスケーラ1024
    OCR0A = 156; // コンペアマッチの値を設定(16MHzクロックの場合、1秒)
    TIMSK0 = (1 << OCIE0A); // コンペアマッチ割り込みを有効化
}

void external_interrupt_init(void) {
    // 外部割り込みの初期化
    EICRA = (1 << ISC01); // フォーリングエッジで割り込み
    EIMSK = (1 << INT0); // INT0を有効化
}

ISR(TIMER0_COMPA_vect) {
    // 割り込みハンドラ:LEDの点滅
    if (led_state) {
        PORTB ^= (1 << PORTB5);
    }
}

ISR(INT0_vect) {
    // 割り込みハンドラ:LEDの点滅を開始または停止
    led_state = !led_state;
}

int main(void) {
    // ポートBの5番ピンを出力に設定
    DDRB |= (1 << DDB5);
    // タイマー0の初期化
    timer0_init();
    // 外部割り込みの初期化
    external_interrupt_init();
    // グローバル割り込みを有効化
    sei();

    while (1) {
        // メインループ
    }

    return 0;
}

このように、タイマーと割り込みを活用することで、効率的なリアルタイム処理を実現できます。次に、通信プロトコルの実装方法について学びます。

通信プロトコルの実装

組み込みシステムでは、他のデバイスとデータをやり取りするために通信プロトコルが重要な役割を果たします。ここでは、よく使われる通信プロトコルとその実装方法を学びます。

UART通信

UART(Universal Asynchronous Receiver/Transmitter)は、シリアル通信プロトコルの一つで、一般的に使用されます。以下は、UARTを使用してデータを送受信する方法の例です。

UARTの初期化

AVRマイクロコントローラーでのUART初期化の例です。

#include <avr/io.h>

void uart_init(unsigned int baud) {
    unsigned int ubrr = F_CPU/16/baud-1;
    UBRR0H = (unsigned char)(ubrr>>8);
    UBRR0L = (unsigned char)ubrr;
    UCSR0B = (1 << RXEN0) | (1 << TXEN0);
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

void uart_transmit(unsigned char data) {
    while (!(UCSR0A & (1 << UDRE0)));
    UDR0 = data;
}

unsigned char uart_receive(void) {
    while (!(UCSR0A & (1 << RXC0)));
    return UDR0;
}

int main(void) {
    uart_init(9600);

    while (1) {
        uart_transmit('A');
        _delay_ms(1000);
    }

    return 0;
}

I2C通信

I2C(Inter-Integrated Circuit)は、短距離通信プロトコルで、複数のデバイス間でデータをやり取りするために使用されます。以下は、I2Cの初期化とデータ送受信の例です。

I2Cの初期化と送信

AVRマイクロコントローラーでのI2C初期化と送信の例です。

#include <avr/io.h>

void i2c_init(void) {
    TWSR = 0x00;
    TWBR = 0x47;
    TWCR = (1 << TWEN);
}

void i2c_start(void) {
    TWCR = (1 << TWSTA) | (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
}

void i2c_stop(void) {
    TWCR = (1 << TWSTO) | (1 << TWEN) | (1 << TWINT);
}

void i2c_write(unsigned char data) {
    TWDR = data;
    TWCR = (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
}

unsigned char i2c_read_ack(void) {
    TWCR = (1 << TWEN) | (1 << TWINT) | (1 << TWEA);
    while (!(TWCR & (1 << TWINT)));
    return TWDR;
}

unsigned char i2c_read_nack(void) {
    TWCR = (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
    return TWDR;
}

int main(void) {
    i2c_init();
    i2c_start();
    i2c_write(0xA0);
    i2c_write(0x00);
    i2c_write(0x55);
    i2c_stop();

    while (1) {
    }

    return 0;
}

SPI通信

SPI(Serial Peripheral Interface)は、高速なデータ転送が可能なシリアル通信プロトコルです。以下は、SPIの初期化とデータ送受信の例です。

SPIの初期化と送信

AVRマイクロコントローラーでのSPI初期化と送信の例です。

#include <avr/io.h>

void spi_init(void) {
    DDRB = (1 << PB3) | (1 << PB5) | (1 << PB2);
    SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0);
}

void spi_write(char data) {
    SPDR = data;
    while (!(SPSR & (1 << SPIF)));
}

char spi_read(void) {
    while (!(SPSR & (1 << SPIF)));
    return SPDR;
}

int main(void) {
    spi_init();
    while (1) {
        spi_write(0x55);
        _delay_ms(1000);
    }

    return 0;
}

通信プロトコルの応用例

以下に、I2Cを使用して温度センサーからデータを読み取る応用例を示します。

#include <avr/io.h>
#include <util/delay.h>

#define TEMP_SENSOR_ADDRESS 0x48

void i2c_init(void) {
    TWSR = 0x00;
    TWBR = 0x47;
    TWCR = (1 << TWEN);
}

void i2c_start(void) {
    TWCR = (1 << TWSTA) | (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
}

void i2c_stop(void) {
    TWCR = (1 << TWSTO) | (1 << TWEN) | (1 << TWINT);
}

void i2c_write(unsigned char data) {
    TWDR = data;
    TWCR = (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
}

unsigned char i2c_read_ack(void) {
    TWCR = (1 << TWEN) | (1 << TWINT) | (1 << TWEA);
    while (!(TWCR & (1 << TWINT)));
    return TWDR;
}

unsigned char i2c_read_nack(void) {
    TWCR = (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
    return TWDR;
}

int main(void) {
    i2c_init();

    while (1) {
        i2c_start();
        i2c_write(TEMP_SENSOR_ADDRESS << 1);
        i2c_write(0x00);
        i2c_start();
        i2c_write((TEMP_SENSOR_ADDRESS << 1) | 1);
        unsigned char temp = i2c_read_nack();
        i2c_stop();

        // tempを処理
        _delay_ms(1000);
    }

    return 0;
}

これらの通信プロトコルを理解し、実装することで、他のデバイスと効率的にデータをやり取りすることができます。次に、センサーデータの取得と処理の具体的な例を学びます。

応用例:センサーデータの取得と処理

組み込みシステムでは、センサーからデータを取得して処理することがよくあります。ここでは、温度センサーからデータを取得し、そのデータを処理する具体的な方法を解説します。

温度センサーの接続

まず、温度センサーをマイクロコントローラーに接続します。ここでは、I2C通信を使用する温度センサー(例えば、LM75)を例にとります。センサーのVCCピンをマイコンの5Vピンに、GNDピンをGNDに、SCLピンをSCLピンに、SDAピンをSDAピンに接続します。

温度データの取得

次に、マイクロコントローラーから温度データを取得するプログラムを作成します。以下の例では、I2Cを使用して温度データを読み取ります。

#include <avr/io.h>
#include <util/delay.h>

#define TEMP_SENSOR_ADDRESS 0x48

void i2c_init(void) {
    TWSR = 0x00;
    TWBR = 0x47;
    TWCR = (1 << TWEN);
}

void i2c_start(void) {
    TWCR = (1 << TWSTA) | (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
}

void i2c_stop(void) {
    TWCR = (1 << TWSTO) | (1 << TWEN) | (1 << TWINT);
}

void i2c_write(unsigned char data) {
    TWDR = data;
    TWCR = (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
}

unsigned char i2c_read_ack(void) {
    TWCR = (1 << TWEN) | (1 << TWINT) | (1 << TWEA);
    while (!(TWCR & (1 << TWINT)));
    return TWDR;
}

unsigned char i2c_read_nack(void) {
    TWCR = (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
    return TWDR;
}

float read_temperature(void) {
    i2c_start();
    i2c_write(TEMP_SENSOR_ADDRESS << 1);
    i2c_write(0x00);
    i2c_start();
    i2c_write((TEMP_SENSOR_ADDRESS << 1) | 1);
    unsigned char msb = i2c_read_ack();
    unsigned char lsb = i2c_read_nack();
    i2c_stop();

    int temp = (msb << 8) | lsb;
    return temp / 256.0;
}

int main(void) {
    i2c_init();

    while (1) {
        float temperature = read_temperature();
        // 温度データを処理
        _delay_ms(1000);
    }

    return 0;
}

データの処理と表示

取得した温度データを処理し、表示します。ここでは、シリアル通信を使ってPCに温度データを送信し、表示する方法を紹介します。

#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>

#define TEMP_SENSOR_ADDRESS 0x48

void uart_init(unsigned int baud) {
    unsigned int ubrr = F_CPU/16/baud-1;
    UBRR0H = (unsigned char)(ubrr>>8);
    UBRR0L = (unsigned char)ubrr;
    UCSR0B = (1 << RXEN0) | (1 << TXEN0);
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

void uart_transmit(unsigned char data) {
    while (!(UCSR0A & (1 << UDRE0)));
    UDR0 = data;
}

void uart_print(const char* str) {
    while (*str) {
        uart_transmit(*str++);
    }
}

void i2c_init(void) {
    TWSR = 0x00;
    TWBR = 0x47;
    TWCR = (1 << TWEN);
}

void i2c_start(void) {
    TWCR = (1 << TWSTA) | (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
}

void i2c_stop(void) {
    TWCR = (1 << TWSTO) | (1 << TWEN) | (1 << TWINT);
}

void i2c_write(unsigned char data) {
    TWDR = data;
    TWCR = (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
}

unsigned char i2c_read_ack(void) {
    TWCR = (1 << TWEN) | (1 << TWINT) | (1 << TWEA);
    while (!(TWCR & (1 << TWINT)));
    return TWDR;
}

unsigned char i2c_read_nack(void) {
    TWCR = (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
    return TWDR;
}

float read_temperature(void) {
    i2c_start();
    i2c_write(TEMP_SENSOR_ADDRESS << 1);
    i2c_write(0x00);
    i2c_start();
    i2c_write((TEMP_SENSOR_ADDRESS << 1) | 1);
    unsigned char msb = i2c_read_ack();
    unsigned char lsb = i2c_read_nack();
    i2c_stop();

    int temp = (msb << 8) | lsb;
    return temp / 256.0;
}

int main(void) {
    uart_init(9600);
    i2c_init();

    while (1) {
        float temperature = read_temperature();
        char buffer[16];
        snprintf(buffer, sizeof(buffer), "Temp: %.2f C\n", temperature);
        uart_print(buffer);
        _delay_ms(1000);
    }

    return 0;
}

このプログラムでは、温度センサーから取得したデータをUART経由でPCに送信し、温度を表示します。このように、センサーデータの取得と処理を行うことで、さまざまな応用が可能になります。次に、演習問題と解答例を提供します。

演習問題と解答例

学んだ内容を確認するために、以下の演習問題に挑戦してみましょう。解答例も併せて提供しますので、自分の理解度を確かめることができます。

演習問題1:デジタル入力と出力

問題:ボタンが押されたときにLEDを点灯させ、ボタンが放されたときにLEDを消灯するプログラムを作成してください。

解答例

#include <avr/io.h>

int main(void) {
    // ポートBの0番ピンを入力に設定
    DDRB &= ~(1 << DDB0);
    // 内部プルアップ抵抗を有効にする
    PORTB |= (1 << PORTB0);
    // ポートBの5番ピンを出力に設定
    DDRB |= (1 << DDB5);

    while (1) {
        // ポートBの0番ピンの状態を読み取る
        if (PINB & (1 << PINB0)) {
            // ボタンが押されていない場合、LEDを消灯
            PORTB &= ~(1 << PORTB5);
        } else {
            // ボタンが押された場合、LEDを点灯
            PORTB |= (1 << PORTB5);
        }
    }

    return 0;
}

演習問題2:タイマーを使用したLED点滅

問題:タイマーを使用して、1秒ごとにLEDを点滅させるプログラムを作成してください。

解答例

#include <avr/io.h>
#include <avr/interrupt.h>

void timer0_init(void) {
    // タイマー0の初期化
    TCCR0A = (1 << WGM01); // CTCモード
    TCCR0B = (1 << CS02) | (1 << CS00); // プリスケーラ1024
    OCR0A = 156; // コンペアマッチの値を設定(16MHzクロックの場合、1秒)
    TIMSK0 = (1 << OCIE0A); // コンペアマッチ割り込みを有効化
}

ISR(TIMER0_COMPA_vect) {
    // 割り込みハンドラ:LEDの点滅
    PORTB ^= (1 << PORTB5);
}

int main(void) {
    // ポートBの5番ピンを出力に設定
    DDRB |= (1 << DDB5);
    // タイマー0の初期化
    timer0_init();
    // グローバル割り込みを有効化
    sei();

    while (1) {
        // メインループ
    }

    return 0;
}

演習問題3:UARTを使用したデータ送信

問題:UARTを使用して、PCに「Hello, World!」というメッセージを1秒ごとに送信するプログラムを作成してください。

解答例

#include <avr/io.h>
#include <util/delay.h>

void uart_init(unsigned int baud) {
    unsigned int ubrr = F_CPU/16/baud-1;
    UBRR0H = (unsigned char)(ubrr>>8);
    UBRR0L = (unsigned char)ubrr;
    UCSR0B = (1 << RXEN0) | (1 << TXEN0);
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

void uart_transmit(unsigned char data) {
    while (!(UCSR0A & (1 << UDRE0)));
    UDR0 = data;
}

void uart_print(const char* str) {
    while (*str) {
        uart_transmit(*str++);
    }
}

int main(void) {
    uart_init(9600);

    while (1) {
        uart_print("Hello, World!\n");
        _delay_ms(1000);
    }

    return 0;
}

演習問題4:I2C通信によるセンサーデータの取得

問題:I2C通信を使用して温度センサー(例えば、LM75)から温度データを取得し、1秒ごとにシリアル通信でPCに送信するプログラムを作成してください。

解答例

#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>

#define TEMP_SENSOR_ADDRESS 0x48

void uart_init(unsigned int baud) {
    unsigned int ubrr = F_CPU/16/baud-1;
    UBRR0H = (unsigned char)(ubrr>>8);
    UBRR0L = (unsigned char)ubrr;
    UCSR0B = (1 << RXEN0) | (1 << TXEN0);
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

void uart_transmit(unsigned char data) {
    while (!(UCSR0A & (1 << UDRE0)));
    UDR0 = data;
}

void uart_print(const char* str) {
    while (*str) {
        uart_transmit(*str++);
    }
}

void i2c_init(void) {
    TWSR = 0x00;
    TWBR = 0x47;
    TWCR = (1 << TWEN);
}

void i2c_start(void) {
    TWCR = (1 << TWSTA) | (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
}

void i2c_stop(void) {
    TWCR = (1 << TWSTO) | (1 << TWEN) | (1 << TWINT);
}

void i2c_write(unsigned char data) {
    TWDR = data;
    TWCR = (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
}

unsigned char i2c_read_ack(void) {
    TWCR = (1 << TWEN) | (1 << TWINT) | (1 << TWEA);
    while (!(TWCR & (1 << TWINT)));
    return TWDR;
}

unsigned char i2c_read_nack(void) {
    TWCR = (1 << TWEN) | (1 << TWINT);
    while (!(TWCR & (1 << TWINT)));
    return TWDR;
}

float read_temperature(void) {
    i2c_start();
    i2c_write(TEMP_SENSOR_ADDRESS << 1);
    i2c_write(0x00);
    i2c_start();
    i2c_write((TEMP_SENSOR_ADDRESS << 1) | 1);
    unsigned char msb = i2c_read_ack();
    unsigned char lsb = i2c_read_nack();
    i2c_stop();

    int temp = (msb << 8) | lsb;
    return temp / 256.0;
}

int main(void) {
    uart_init(9600);
    i2c_init();

    while (1) {
        float temperature = read_temperature();
        char buffer[16];
        snprintf(buffer, sizeof(buffer), "Temp: %.2f C\n", temperature);
        uart_print(buffer);
        _delay_ms(1000);
    }

    return 0;
}

これらの演習問題を通じて、基本的な入出力操作、タイマーと割り込み、UARTおよびI2C通信の実装方法を実践的に学ぶことができます。次に、記事全体の内容を振り返り、まとめを行います。

まとめ

この記事では、C言語を用いた組み込みシステム開発の基本から応用までを解説しました。組み込みシステムの基本概念、C言語の基本構文、開発環境の構築、マイクロコントローラーの基礎知識、基本的な入出力操作、タイマーと割り込み処理、通信プロトコルの実装、センサーデータの取得と処理、そして演習問題を通じて学びました。

これらの基礎知識と実践的なスキルを習得することで、さまざまな組み込みシステム開発プロジェクトに対応できるようになります。継続して実践し、応用力を高めることで、より複雑なシステムの開発も可能となります。次のステップとしては、実際のプロジェクトに取り組んでみたり、さらなる高度な技術を学んだりすることをお勧めします。

コメント

コメントする

目次