MCR - Micom Car Rally
2015大会 マイコンカーラリーとは? 今から始めるマイコンカーラリー 技術情報 大会記録 MCRファン倶楽部 お問い合せ
マイコンカーラリートップへ マイコンカーラリートップへ
技術情報 >> R8C/M12A 技術情報
技術情報
アプリケーション
R8C/M12A 技術情報
上位入賞マイコンカー
参加者レポート
よくある質問(FAQ)
ダウンロード
R8C/M12A 技術情報
UART0による通信

内容
R8C/M12Aの内蔵周辺機能のひとつであるUART0を使った実習です。UARTとは、「汎用クロック同期形&非同期形送受信機:Universal Synchronous & Asynchronous Receiver Transmitter」のことで、送信線、受信線、GNDの3本(同期形の場合は、クロック線を入れた4本)で、様々な機器とデータのやり取りをすることができます。今回の「UART0」の「0」は、チャンネル0番という意味です。といっても、R8C/M12Aには0番しかありません。R8Cの違うマイコンは、UART0の他にUART1やUART2のあるマイコンもあります。
今回は、UART0を使って、パソコンに文字を送ったり、パソコンからマイコンにデータを送ったりします。

回路
回路を下図に示します。赤丸で囲った配線が追加した部分です。P1_6につながっていたLEDは取ります

回路図

ブレッドボード実装図
ブレッドボードの実装を下図に示します。
ブレッドボード実装図

ワークスペース

r8cm12a_uart0_101.zip
ファイルを解凍し、フォルダを「c:\worksapce」に入れてください。

シリアル通信とは
マイコン内部では、「バス」という線が1組8本あり、この線でマイコン内部の各機能同士がデータのやり取りを行っています。簡単に、データのやり取りができて良いのですが、機能同士の距離が長ければ長いほど多くの線が必要です。そこで、1本の線にして、8本線の情報を順番に送受信するのがシリアル通信です(下図)。

シリアル通信の仕組み

1本線でデータのやりとりを行おうとしても、通信する速度や手順を決めておかなければ受け取った相手は意味がわからず、こちらも受け取ったデータの意味が分かりません。よって、通信する速度や手順を決めて通信します。これらのことをUART0で行います。
データを送るとき、文字データだけ送るわけではありません。受信側で正しく受け取るために送信側は、「スタートビット、データ、パリティビット、ストップビット」という形で送ります(下図)。

シリアル通信の仕組み
※LSB…Least Significant Bit(最下位ビット)のことです。
※MSB…Most Significant Bit(最上位ビット)のことです。

●通信速度(ボーレート)
1秒間に何ビット分のデータを送るか設定します。単位は、「Bits Per Second」で略して「bps(ビーピーエス)」です。
例えば9600bpsは、1秒間に9600個のビットを送ることができます。Tera Term(今回の実習で使用するフリーの通信ソフト)で設定できる通信速度と特徴を下記に示します。

シリアル通信の仕組み

●ストップビット
ストップビットは必ず"0"です。ちなみに、データが無いときは通信線は"1"になっています。"1"→"0"になると通信スタートと判断して、設定されている通信速度でデータを送信、または受信します。

●データ長
データのビット数を設定します。7ビット、8ビット、9ビットなどがあります。1文字=8ビットなので、8ビットにすることがほとんどです。

●パリティビット
パリティビットは、データが正しいかどうか誤り検出をするためのビットです。パリティ(parity)とは、「等しいこと」です。パリティビットは、無し/偶数パリティ/奇数パリティがあり、無しのときは、このビット自体がありません。

・偶数パリティに設定した場合
データ8ビット分+パリティビットの各ビットの"1"の数が偶数になるようにパリティビットを設定して、送信します。受信側にも、"1"の数が偶数と設定しておけば、偶数ならエラー無し、奇数ならエラーと判断できます。エラーなら、パリティエラーとなります(下図)。

シリアル通信の仕組み

・奇数パリティに設定した場合
データ8ビット分+パリティビットの各ビットの"1"の数が奇数になるようにパリティビットを設定して、送信します。受信側にも、"1"の数が奇数と設定しておけば、奇数ならエラー無し、偶数ならエラーと判断できます。エラーなら、パリティエラーとなります(下図)。

シリアル通信の仕組み

●ストップビット
ストップビットは必ず"1"です。1文字の通信が終わったことを示します。ビット幅は1ビット、1.5ビット、2ビットなどがあります。通常は1ビットです。受信側でストップビットを検出できない場合は、フレーミングエラーとなります。

●フロー制御
フロー制御とは、受信側で受信処理が間に合わない、または受信準備が整っていないときに、送信側に送信を待つように知らせ、受信準備ができたら送信を行うように知らせる機能です。
 フロー制御を行うには、送信側、受信側にフロー制御を行うプログラムが入っていなければいけません。今回のプログラムは、フロー制御は行いません。


今回は、データ長:8bit、パリティビット:無し、ストップビット:1bit、にします。

パソコンとマイコンの接続
現在のパソコンには、外部機器(プリンタや外付けハードディスク、外付けDVDドライブなど)と接続するコネクタとしてUSBが必ず付いています。パソコンのUSBコネクタとR8C/M12Aマイコンをつないで通信できれば良いのですが、USBの通信は複雑でR8C/M12AマイコンはUSB通信ができません。
昔のパソコンにはD-Sub9ピンコネクタがありました。OS(Windowsなど)はこれをCOMポートとして認識します。COMポートとは「Communication Port」の略で、PC/AT互換機のRS-232C用シリアルポートのことです。R8C/M12AマイコンにはUART0というシリアル通信機能があり、パソコンのCOMポートとならR8C/M12Aマイコンはシリアル信号で通信することができます(ただし、電圧変換回路は必要)。
そこで、USB信号をTTLレベル(R8C/M12Aマイコンと直結できる電圧)のシリアル信号に変換する基板で信号を変換します。変換後のシリアル信号をR8C/M12Aマイコンの2端子(GNDを入れると3端子)につなげることにより、パソコンと通信することができます。この変換基板が「RY-WRITER基板」です。この基板を使ってプログラムの書き込みも行っていますので、書き込み基板と勘違いされがちですが、この基板は通信するための基板です。

■パソコン、RY-WRITER基板、R8C/M12Aの接続

パソコンにRY-WRITER基板を接続すると、パソコンからはCOMポートとして認識されます(下図)。

デバイスマネージャー

デバイスマネージャーでCOMポートとして認識したところを下記に示します。

デバイスマネージャー

上画面の例では「COM4」と認識されました。書き込みソフトやTeraTermで通信するとき、始めにCOM番号を設定しなければいけないので、接続したRY-WRITER基板のCOM番号が何番か知っておく必要があります。ただし、今回使用する書き込みソフト「R8C Writer」は自動的に番号を探すので、番号を調べる必要はありません。

■書き込み時とプログラム実行時の通信端子

書き込み時、R8C/M12Aの受信端子はP1_6、送信端子はP1_4です(下図)。この端子は指定なので変更できません。

書き込み時の端子

プログラム実行時、R8C/M12Aの受信端子はP1_5、送信端子はP1_4です(下図)。本当は受信端子をP1_6にできれば良いのですが、残念ながらこの端子を受信端子にすることはできません。
※P1_5、P1_4は今回のUART0の端子設定です。受信端子、送信端子は選べます(選択方法は後述のプログラムの説明をご覧ください)。

プログラム実行時の端子

書き込み時とプログラム実行時では、受信端子が変わります。書き込み、プログラム実行のたびに受信端子を変えるのは大変なので、今回の実習ではP1_5端子とP1_6端子をつないで実習します(下図)。R8C/M12Aは端子が少ない中、受信で2端子も使用してしまうので、何かのシステムに組み込む場合は、P1_5端子とP1_6端子を切り離して使用してください。

プログラム実行時の端子
※書き込み時、P1_5端子は入力端子になっています。プログラム実行時、P1_6端子は必ず入力端子にしてください。

プログラムの説明

UART0の設定

UART0の概要を、下図に示します。

UART0 概要

今回はUART0を使って、パソコンと情報のやり取りを行います。これからUART0の設定について説明します。

■RXD0端子の選択
RXD0端子は、P1_4端子、P1_5端子、P4_6端子の3つあります。どの端子を使うかは、プログラムで設定します。
P1_4端子をRXD0端子にしたい場合、ポートP1_4機能選択ビット(P14SEL2,P14SEL1,P14SEL0)を下記のように設定します。
    p14sel2 = 0;  /* P1_4 = RXD0端子にする        */
    p14sel1 = 1;
    p14sel0 = 0;

P1_5端子をRXD0端子にしたい場合、ポートP1_5機能選択ビット(P15SEL2,P15SEL1,P15SEL0)を下記のように設定します。
    p15sel2 = 0;  /* P1_5 = RXD0端子にする        */
    p15sel1 = 0;
    p15sel0 = 1;

P4_6端子をRXD0端子にしたい場合、ポートP4_6機能選択ビット(P46SEL2,P46SEL1,P46SEL0)を下記のように設定します。
    p46sel2 = 0;  /* P4_6 = RXD0端子にする        */
    p46sel1 = 0;
    p46sel0 = 1;

今回は、P1_5をRXD0端子にしています。

■TXD0端子の選択
TXD0端子は、P1_4端子、P4_2端子、P4_6端子の3つあります。どの端子を使うかは、プログラムで設定します。
P1_4端子をTXD0端子にしたい場合、ポートP1_4機能選択ビット(P14SEL2,P14SEL1,P14SEL0)を下記のように設定します。
    p14sel2 = 0;  /* P1_4 = TXD0端子にする        */
    p14sel1 = 0;
    p14sel0 = 1;

P4_2端子をTXD0端子にしたい場合、ポートP4_2機能選択ビット(P42SEL1,P42SEL0)を下記のように設定します。
    p42sel1 = 1;  /* P4_2 = TXD0端子にする        */
    p42sel0 = 0;

P4_6端子をTXD0端子にしたい場合、ポートP4_6機能選択ビット(P46SEL2,P46SEL1,P46SEL0)を下記のように設定します。
    p46sel2 = 0;  /* P4_6 = TXD0端子にする        */
    p46sel1 = 1;
    p46sel0 = 0;

今回は、P1_4をTXD0端子にしています。


■UART0送受信モードレジスタ(U0MR)の設定
bit 7 6 5 4 3 2 1 0
設定
内容
  パリティ
許可ビット
0:パリティ
禁止
1:パリティ
許可
パリティ
奇数/偶数
選択ビット
0:奇数
パリティ
1:偶数
パリティ
ストップ
ビット長
選択ビット
0:1ストップ
ビット
1:2ストップ
ビット
内部/外部
クロック選択
ビット
0:内部
クロック
1:外部
クロック
シリアルI/Oモード選択ビット
000:シリアルインタフェース無効
001:クロック同期形シリアル
I/Oモード
100:UARTモード転送
データ長7ビット
101:UARTモード転送
データ長8ビット
110:UARTモード転送
データ長9ビット
設定値 0 0 0 0 0 1 0 1

パリティは無しにします。ストップビット長は1ビットにします。シリアルI/Oモードは、UARTモード転送データ長8ビットにします。このモードは、クロックを使わないのでクロック選択は関係ありません。

■UART0送受信制御レジスタ0 (U0C0)
bit 7 6 5 4 3 2 1 0
設定
内容
転送フォーマット選択ビット
0:LSBファースト
1:MSBファースト
  データ出力選択ビット
0:TXD0端子はCMOS出力
1:TXD0端子はNチャネルオープンドレイン出力
RXD0デジタルフィルタ許可ビット
0:デジタルフィルタ禁止
1:デジタルフィルタ許可
    U0BRG
カウントソース
選択ビット
00:f1(20MHz)
01:f8(2.5MHz)
10:f32(0.625MHz)
設定値 0 0 0 1 0 0 0 0

RXD0デジタルフィルタ許可ビットは、RXD0端子から入力された信号が、3クロック以上、同じレベルなら信号とみなし、それ以下ならノイズと見なして無視する機能です。今回は許可します。
U0BRGカウントソース選択ビットは、次に説明します。

■UART0ビットレートレジスタ(U0BRG)の設定
bit 7 6 5 4 3 2 1 0
9600bpsなら129を設定

UART0送受信制御レジスタ0 (U0C0)のU0BRGカウントソース選択ビットと組み合わせて設定します。
通信速度を設定します。計算式を下記に示します。

U0BRG=
U0BRGカウントソース選択ビットの設定÷(設定したいビットレート×16)−1
※U0BRGは、0〜255の値にしてください。

一般的な通信速度は、9600bps、19200bps、38400bsp、115200bpsなどです。TeraTermなどのパソコンの通信ソフトの標準は9600bpsが多いので、今回は9600bpsに設定することにします。よって、「設定したいビットレート=9600」になります。「U0BRGカウントソース選択ビット」は、00を設定しているので20MHzになります。計算すると

U0BRG = 20×106÷(9600×16)−1 = 129.21 ≒ 129

となります。設定は整数なので、四捨五入します。
結果が、256以上になる場合は、U0BRGカウントソース選択ビットをf8やf32に変えてください。f32でもU0BRGが256以上なら、設定しようとしている通信速度は設定できません。

■UART0送受信制御レジスタ1(U0C1)の設定
bit 7 6 5 4 3 2 1 0
設定
内容
        受信完了フラグ
0:U0RBレジスタにデータなし
1:U0RBレジスタにデータあり
受信許可ビット
0:受信禁止
1:受信許可
送信バッファ空フラグ
0:U0TBレジスタにデータあり
1:U0TBレジスタにデータなし
送信許可ビット
0:送信禁止
1:送信許可
設定値           1   1
ビットの
名称
        RI_U0C1 RE_U0C1 TI_U0C1 TE_U0C1

受信許可ビット、送信許可ビットをそれぞれ"1"にすると、RXD0端子と受信回路、TXD0端子と送信回路がそれぞれつながり、送受信できるようになります。

■UART0の設定プログラム
UART0の設定は、init関数内で行います。プログラムを下記に示します。

    /* UART0の設定 */
    mstuart = 0;            /* UART0を有効にする            */

    p15sel2 = 0;            /* P1_5 = RXD0端子にする        */
    p15sel1 = 0;
    p15sel0 = 1;

    p14sel2 = 0;            /* P1_4 = TXD0端子にする        */
    p14sel1 = 0;
    p14sel0 = 1;

    u0mr = 0x05;            /* UARTモード8bit               */
                            /* 1ストップビット,パリティなし         */
    u0c0 = 0x10;            /* U0BRGカウントソース:f1              */
    u0brg = 129;            /* f1 / (bps*16) - 1            */
                            /* = 20*10^6/(9600*16)-1=129.2  */
    te_u0c1 = 1;            /* 送信許可ビット:送信許可      */
    re_u0c1 = 1;            /* 受信許可ビット:受信許可      */

「mstuart」は、UART0スタンバイビットで、UART0を有効にするか、無効にするかを選択するビットです。0で有効です。初期状態ではUART0は無効になっているので、UART0を使う前にこのビットを0にしてください。

1文字受信するget_uart0関数

■関数
書式 int get_uart0( char *s );
内容 UART0のRXD端子からシリアルデータを受信します。
引数 受信データを保存するポインタ
戻り値 1: 受信データありです。引数で指定した変数には、受信した1文字が格納されます。
0:受信データなしです。引数で指定した変数には、何も代入されません。
-1:受信エラーです。何かの問題で、うまく受信できませんでした。引数で指定した変数には、受信した1文字が格納されていますが、エラーで受信した不完全なデータが格納されます。
使用例
  nsigned char c;  // 受信した値
  int ret;         // 受信結果

  // 受信文字があればcに代入
  ret = get_uart0( &c );

  // 受信するまで待つ
  while( get_uart0( &c ) );

■プログラム
UART0送受信制御レジスタ1(U0C1)の受信完了フラグ(RI_U0C1)をチェックするとデータを受信しているか分かります。このビットが"1"ならデータを受信しているので、データを取り出します。データは、UART0受信バッファレジスタ(U0RB)に格納されます。
get_uart0関数のプログラムを下記に示します。

int get_uart0( char *s )
{
    int ret = 0, data;
    char c;

    if (ri_u0c1 == 1){        /* 受信データあり?             */
        data = u0rb;
        *s = (char)data;      ←bit7〜bit0を読み込む
        ret = 1;
        if( data & 0xf000 ) { /* エラーあり?                 */
            /* エラー時は再設定 */
            re_u0c1 = 0;
            te_u0c1 = 0;
            c = u0mr;
            u0mr &= 0xf8;
            u0mr = c;
            re_u0c1 = 1;
            te_u0c1 = 1;
            ret = -1;
        }
    }
    return ret;
}

■UART0受信バッファレジスタ(U0RB)の設定
bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
内容 エラーサムフラグ
0:エラーなし
1:エラー発生
パリティエラーフラグ
0:パリティエラーなし
1:パリティエラー発生
フレーミングエラーフラグ
0:フレーミングエラーなし
1:フレーミングエラー発生
オーバランエラーフラグ
0:オーバランエラーなし
1:オーバランエラー発生
        受信データ

bit7〜bit0を読み込めば、受信データが分かります。bit15〜12を読み込めば、受信エラーがあるか分かります。ルネサス エレクトロニクスの「R8C/M11Aグループ、R8C/M12Aグループ ユーザーズマニュアル ハードウェア編」には、「U0RBレジスタは、16ビット単位で読み出してください。」とありますので、U0RBを16bit分読み出してからbit15〜12をチェックし、エラーがなければbit7〜0を読み込みます。エラーがあった場合、マニュアルに記載してある方法で復帰します。復帰方法を下記に示します。

クロック同期形シリアルI/O モードで受信または送信時に通信を途中終了させた場合、または通信 エラーが発生した場合、次の手順で設定してください。
(1) U0C1レジスタのTEビットを0 (送信禁止)、REビットを0 (受信禁止)にする
(2) U0MRレジスタのSMD2〜SMD0ビットを000b (シリアルインタフェース無効)にする
(3) U0MRレジスタのSMD2〜SMD0ビットを001b (クロック同期形シリアルI/O モード)にする
(4) U0C1レジスタのTEビットを1(送信許可)、REビットを1 (受信許可) にする


1文字送信するput_uart0関数

■関数
書式 int put_uart0( char r );
内容 UART0のTXD0端子からシリアルデータを送信します。
引数 送信データ
戻り値 1:送信セット完了です。UART0は、送信を開始します。
0:前回、送信したデータを送信中のため、今回のデータは送信できません。
使用例
  nsigned char c;  // 送信する値
  int ret;         // 受信結果

  // 送信(ret=1なら実際に送信、0なら送信せず)
  c = 'a';
  ret = put_uart0( c );

  // 送信するまで待つ
  while( put_uart0( c ) == 0 );

■プログラム
UART0送受信制御レジスタ1(U0C1)の送信バッファ空フラグ( TI_U0C1)が"1"なら送信中のデータがないので、データを送信します。データはUART0送信バッファレジスタ(U0TB)にセットします。
get_uart0関数のプログラムを下記に示します。

int put_uart0( char r )
{
    if(ti_u0c1 == 1) {  /* 送信データなし?             */
        u0tbl = r;
        return 1;
    } else {
        /* 先に送信中(今回のデータは送信せずに終了) */
        return 0;
    }
}

■UART0送信バッファレジスタ(U0TB)の設定
bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
内容                 送信データを書き込み

bit7〜bit0に送信データを書き込みます。U0TBは上位8bitと下位8bitに分かれていて、それぞれU0TBH(U0TB Hight)とU0TBL(U0TB Low)という名前です。送信データ書き込みは下位8bitなのでU0TBLに送信データを書き込みます。

文字列を出力するput_uart0_str関数

put_uart0関数は、1文字データを出力する関数です。もし10文字出力したい場合は、この関数を10回実行します。ただし、これでは大変なので、文字列を出力する専用のput_uart0_str関数を用意しました。

■関数
書式 void put_uart0_str( char *str );
内容 引数で指定したアドレスのデータを送信します。
引数 送信データのポインタ
戻り値 無し。送信するまでし終わるまでこの関数は終わりません。
使用例
    put_uart0_str( "R8C/M12Aマイコン 10進数表示です!\n" );

■プログラム
void put_uart0_str( char *str )
{
    while(*str != '\0') {
        /* '\n'は'\r\n'に変換する */
        if( *str == '\n' ) while( !put_uart0( '\r' ) );
        while( !put_uart0( *str ) );    /* 1文字送信      */
        str++;
    }
}

10進数文字列を出力するput_uart0_num関数

変数の値を10進数の文字列に変換し送信します。

■関数
書式 void put_uart0_num( long value, int keta );
内容 変数の値を10進数の文字列に変換し送信します。
引数 long 変換する値
int 桁の数(1〜8)
戻り値 無し。送信するまでし終わるまでこの関数は終わりません。
使用例
    long data = 12345678;
    put_uart0_num( data, 8 );

■プログラム
void put_uart0_num( long value, int keta )
{
    int i = keta;
    long temp = 0;

    do {
        temp <<= 4;
        temp += value % 10;
        value /= 10;
    } while( --i );

    do {
        while( !put_uart0( (temp & 0xf) + '0' ) ); /*1文字送信*/
        temp >>= 4;
    } while( --keta );
}

16進数文字列を出力するput_uart0_num関数

変数の値を16進数の文字列に変換します。

■関数
書式 void put_uart0_hex( unsigned long value, int keta );
内容 変数の値を16進数の文字列に変換し送信します。
引数 unsigned long 変換する値
int 桁の数(1〜8)
戻り値 無し。送信し終わるまでこの関数は終わりません。
使用例
    long data = 0x1a2b3c4d;
    put_uart0_hex( data, 8 );

■プログラム
void put_uart0_hex( unsigned long value, int keta )
{
    char temp;

    do {
        temp = value >> (keta * 4 - 4);
        temp &= 0x0f;
        temp += (temp < 10) ? '0' : ('a'-10);

        while( !put_uart0( temp ) );  /* 1文字送信  */
    } while( --keta );
}

main関数

main関数を下記に示します。

void main( void )
{
    unsigned char c;
    int ret;

    init();       /* 初期化                       */

#if 1
    // 例題1
    while( 1 ) {
        ret = get_uart0( &c );
        if( ret == 1 ) put_uart0( c );
    }
#endif

#if 0
    // 例題2
    put_uart0_str( "R8C/M12Aマイコン 10進数表示です!\n" );
    put_uart0_num( 12345678, 8 );
    while( 1 );
#endif

#if 0
    // 例題3
    put_uart0_str( "R8C/M12Aマイコン 16進数表示です!\n" );
    put_uart0_hex( 0x1a2b3c4d, 8 );
    while( 1 );
#endif
}

「#if 値」〜 「#endif」で、値が0なら「#if〜#endif」までのプログラムを無視します。値が1なら、普通にビルドします。
例題1を実行するときは、例題1部分の#ifを1に、その他の#ifを0にしてください(上記プログラムの状態です)。例題1は、パソコンなどの外部機器から送られてきたデータを受信、そのデータをそのまま出力します。これは「ループバックテスト」といい、受信、送信が正常にできているか確認するときによく行われます。
例題2を実行するときは、例題2部分の#ifを1に、その他の#ifを0にしてください。例題2は、文字列の出力と、変数の値を10進数に変換して出力する例題です。
例題3を実行するときは、例題3部分の#ifを1に、その他の#ifを0にしてください。例題3は、文字列の出力と、変数の値を16進数に変換して出力する例題です。

通信ソフトを使って通信しよう

■TeraTermのインストール
COMポートを使ったシリアル通信を行うソフトとして、今回は「Tera Term」を使います。Tera Termは、ダウンロードサイトからダウンロード、インストールしてください。

TeraTermを立ち上げると「新しい接続」画面が立ち上がりますので、「シリアル」を選択して、COMポート番号を選びます。RY-WRITER基板と接続していると「COM○:Prolific USB-to-Serial Comm Port」という選択がでてきますので、これを選択してOKをクリックしてください(○は数字が入ります)。
TeraTerm

ブレッドボードのSW2を押しながらSW1を下にすると(SW1が下ならSW2を押すだけです)、受信した文字を送信するプログラムが実行されます。TeraTerm上で何かの文字を入力すると、押した文字が表示されます。下図は、「abcdefg」と入力したところです。
TeraTerm

勘違いしやすいのは、TeraTermはあくまでCOMポートから受信した文字を表示しているだけで、キーボードに入力した文字を表示しているわけではないということです(下図の@→A→Bの順に信号が進んでいきます)。Wordなどのワープロソフトは、キーボードに入力した文字を表示していますので、全く原理が違います。

TeraTerm

試しに、ブレッドボードとRY-WRITER基板をつないでいるCN1を外して、TeraTerm上でキーボードに何か文字を入力してみてください。文字を入力しても何も表示されません。

BACK