ラベル USART の投稿を表示しています。 すべての投稿を表示
ラベル USART の投稿を表示しています。 すべての投稿を表示

2016年1月9日土曜日

STM32F1のUSARTを割り込みで送信

USARTの送信をポーリングで行う場合、115.2kbaudで32文字送った場合、2.8msec程度かかる。わずかといえばわずかだが、この時間はほとんど無駄にループさせているため、CPUのリソースが浪費されている。一旦データをバッファにコピーし、送信処理を終了してから、割り込みなどを利用してデータを送ればこの時間を200usec未満にすることができる。

1) 変数を用意する

#define USART1_TX_Buff_Size (128)
uint8_t USART1_TX_Buff[USART1_TX_Buff_Size];
uint16_t USART1_TX_Buff_SetCounter = 0;
volatile uint16_t USART1_TX_Buff_GetCounter = 0; // 割り込みで更新するのでvolatileをつける 

USART1_TX_Buff_Sizeでバッファサイズを設定する。その後USART1_TX_Buffでバッファを用意し、SetCounterとGetCounterを用意する。Setは挿入位置、Getは取り出し位置を記録する。GetCounterは割り込みの中で変更されるため、コンパイラの最適化によってwhileによる監視等が削除される場合がある。そのためGetにはvolatileをつけて最適化を回避する。

2) 下位関数を用意する

uint16_t USART1_TX_Buff_SetCounter_Inc(void) {
    uint16_t counter = USART1_TX_Buff_SetCounter;

    counter++;

    if (counter == USART1_TX_Buff_Size) {
        counter = 0;
    }

    return(counter);
}

uint16_t USART1_TX_Buff_GetCounter_Inc(void) {
    uint16_t counter = USART1_TX_Buff_GetCounter;

    counter++;

    if (counter == USART1_TX_Buff_Size) {
        counter = 0;
    }

    return(counter);
}

SetCounter_IncとGetCounter_Incはその名の通りSetとGetをインクリメントする。ただし関数内では変数に反映せず、戻り値として値を返すので関数を呼び出したところで責任をもって変数に反映する必要がある。

3) 割り込みを有効にする

    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 5;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    USART_ITConfig(USART1, USART_IT_TC, ENABLE);

USARTを初期化する際にNVICも初期化する。USART自体の初期化は前回のエントリを参照。

4) 割り込みハンドラを用意する

void USART1_IRQHandler(void)
{
    if (USART_GetITStatus(USART1, USART_IT_TC))
    {
        if (USART1_TX_Buff_SetCounter == USART1_TX_Buff_GetCounter) {
            USART_ITConfig(USART1, USART_IT_TC, DISABLE);
        } else {
            USART_SendData(USART1, USART1_TX_Buff[USART1_TX_Buff_GetCounter]);

            USART1_TX_Buff_GetCounter = USART1_TX_Buff_GetCounter_Inc();

            USART_ClearITPendingBit(USART1, USART_IT_TC);
        }
    }
}

内部処理としてはSetとGetの差が0なら割り込みを停止し、割り込み自体はクリアせずに終了する。データが存在する場合はUSART_SendDataで送信を行い、Getをインクリメントする。その後割り込みをクリアして終了する。

5) 送信用の関数を用意する

void USART1_putc(unsigned char ch) {
    uint16_t inc;
    
    do {
        inc = USART1_TX_Buff_SetCounter_Inc();
    } while (inc == USART1_TX_Buff_GetCounter);

    USART1_TX_Buff[USART1_TX_Buff_SetCounter] = ch;

    USART1_TX_Buff_SetCounter = inc;

    USART_ITConfig(USART1, USART_IT_TC, ENABLE);
}

putcの中ではバッファへのコピーのみを行う。ただし未送信のデータを破壊しないようにdo whileで待機する。その後にバッファへコピーを行い、SetCounterを更新する。最後に割り込みを有効にする。


割り込みはデータの流れが面倒になるのと、ソフトウェアもそれなりに複雑になる。毎度のことだがほんとうに必要かどうかを考えてから実装しよう。

STM32F1のUSARTをDMAで受信

USARTを受信するにはポーリングでは不都合。とりあえずタイマ割り込みを使うのが手っ取り早いが、タイマ割り込みではデータが来るたびに割り込み処理を行う必要がある。DMAを使うとソフトウェアではほとんど処理する必要が無いため、気分的に早くなってる気がする。

1) 変数を用意する

#define USART1_RX_Buff_Size (256)
uint8_t USART1_RX_Buff[USART1_RX_Buff_Size];
uint16_t USART1_Rx_Buff_GetCounter = USART1_RX_Buff_Size;

今回はバッファサイズを256としたが、USART1_RX_Buff_SizeをGCCのオプションで設定すればmakefileとかで変更することもできると思う。バッファサイズを小さくし過ぎると取りこぼすデータが発生するかもしれない。上限は数十kbyteあたりだが、実用上は数百バイトもあれば十分だろう。

2) USARTその他を初期化する

void USART1_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    DMA_InitTypeDef DMA_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOA, &GPIO_InitStructure);


    USART_InitStructure.USART_BaudRate = 115200;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_Init(USART1, &USART_InitStructure);


    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->DR);
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)(USART1_RX_Buff);
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = sizeof(USART1_RX_Buff) / sizeof(USART1_RX_Buff[0]);
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

    DMA_DeInit(DMA1_Channel5);
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);
    DMA_Cmd(DMA1_Channel5, ENABLE);

    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);


    USART_Cmd(USART1, ENABLE);
}

まずGPIOとUSARTを初期化する。これはポーリングでの受信や割り込みでの受信と全く同じ。TXのPin9はAF_PP、RXのPin10はPullUpで初期化する。
ペリフェラルを初期化したらDMAの初期化を行う。DMAの初期化も特に変なところはないが、ModeをCircularに設定する。優先度はMediumとしたが、例えばSPIを最大速度でVeryHighにした場合はUSARTを取りこぼすかもしれない。USARTは非同期で送信されるため、可能なかぎり優先度を上げておくほうがいい。
それと大事なことだが、DMAを初期化する前にDMAのクロックを有効にしておく必要がある(RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);)。

3) 受信関数

int USART1_getc(void) {
    int counter = DMA_GetCurrDataCounter(DMA1_Channel5);

    if (counter == USART1_Rx_Buff_GetCounter) {
        return(-1);
    }

    uint8_t ch = USART1_RX_Buff[USART1_RX_Buff_Size - USART1_Rx_Buff_GetCounter--];

    if (USART1_Rx_Buff_GetCounter == 0) {
        USART1_Rx_Buff_GetCounter = USART1_RX_Buff_Size;
    }

    return(ch);
}

この関数ではDMAを確認し、データがない場合は-1を返す。データが有る場合は最初に受信したデータを返す。この関数はバッファからデータを読む機能しかない。1行を受信したい場合は上位の関数で改行文字を検出したりする必要がある。



今回はUSARTの受信をDMAで行った。割り込みを使う場合と比べてちょっとくらい早くなった気がする。
USARTの受信はそんなに大変じゃないけど、送信は結構面倒。ただ送信をバッファリングできれば数msecとか数十msec単位で高速化できたりするので試してみる価値はある。