2016年7月7日木曜日

STM32F1のUSART(DMA)

STM32F1にFreeRTOSをポーティングしたついでにUSARTもいじってる。他のサンプルを見てないのでどういうやり方が正しいOSの使い方なのかわからないけど。

ハードウェアの初期化部分は通常の非OS環境と同じように使っていて、非OS環境でも使えるので貼っておく。




まずペリフェラルの初期化。一つの関数でGPIO, USART, DMA, NVICの初期化を行っている(なので長い)。

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

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, 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_High;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    
    DMA_DeInit(DMA1_Channel5);
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);
    DMA_Cmd(DMA1_Channel5, ENABLE);


    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->DR);
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)(USART1_TX_Buff);
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
    DMA_InitStructure.DMA_BufferSize = 1;
    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_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    
    DMA_DeInit(DMA1_Channel4);
    DMA_Init(DMA1_Channel4, &DMA_InitStructure);
    
    
    NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel4_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 10;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE);
    
    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
    
    USART_Cmd(USART1, ENABLE);
}

「関数の最初に使用する変数を宣言するのはレガシーコーダー(以下略」とか批判?があるみたいだけど、個人的には関数内で何をやってるか理解しやすいので気に入ってる。例えばこの関数だと最初にGPIO_InitTypeDef, USART_InitTypeDef, DMA_InitTypeDef, NVIC_InitTypeDefの変数を宣言しているから、「GPIOとUSARTとDMAとNVICを初期化するんだな」ってすぐに分かる。もっとも、中身が理解できないほど長い関数を作るな、と言われたらそのとおりなんだけど。

さて、この関数では最初にGPIOを初期化する。TXピンはAF_PPで、RXピンはPullUpで初期化する。その後でUSARTを初期化する。ここまでは通常のUSARTと同じ。

次にDMAの初期化だが、今回は送信・受信共にDMAを使用している。1バイト転送毎に割り込みを翔けるより、DMAで一気に転送したほうがCPUで触る回数が少ないため。
DMA1-5が受信バッファ、DMA1-4が送信バッファで、送信バッファと受信バッファはグローバル変数で宣言してある。受信バッファは循環、送信バッファは非循環(Normal)で初期化する。
また受信DMAはBufferSizeに受信バッファの大きさを設定するが、送信バッファは1を指定する。これは送信する文字列ごとに長さが変わるため。BufferSize0では構造体のチェックで弾かれるのでダミーの数字を入れておく。
受信DMAはデータを1バイト受信するごとに1回転送するので、何も受信していない場合は待機となる。なのでこの時点でDMAを有効にしておく。対して送信DMAはUSARTのデータバッファが空き次第どんどん送っていくので、DMAを有効にするのは送信時に行う。

次に割り込みの初期化を行う。といっても受信ではなく送信側。これは後で説明するが、普通の割り込みと同じ。

最後にUSARTを有効にして終了する。


次に送受信を行う機能だが、とりあえず簡単な方から説明する。ということで受信の処理。

int USART1_getc(void) {
    uint16_t 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);
}

この関数をでは、まずバッファの現在位置を確認し、データが入っていないと判断すると-1を返す。データが有った場合は位置を移動してからデータを返す。そんなに難しい処理ではないと思う。
注意点として、ある程度定期的にこの関数でデータのチェックを行っておくこと。DMA側はバッファの中身を気にせずにどんどん受信していくから、バッファ数以上のデータを受け取ると古いデータを破壊して新しいデータを受信する。また、一旦データを破壊すると現在位置の位置関係がおかしくなるので、バッファ全体を読み捨てないと破壊されたデータが読み出されることになる。もちろんデータが破壊されてるかは(大抵の場合)ソフトウェアで判断することができないので、破壊されないようにする一番手っ取り早い手段は定期的にポーリングしておく事となる。
例えば115.2kbpsで通信している場合、250バイトを転送するにはおよそ20msecかかるので、10msec毎にバッファがカラになるまで読み出せば良いはず。


最後に送信の処理だが、これはかなり面倒。一応Newlibで使えるように_write_rに実装してあるが、すべてのデータをUSARTに出すだけのやっつけ実装。

_ssize_t _write_r(
    struct _reent *r,
    int file,
    const void *ptr,
    size_t len)
{
    
    int i;
    const char* p = (char*)ptr;
    
    for (i = len; i > 0; ) {
        int l = i;
        if (l > USART1_TX_QUEUE_SIZE - 1) {
            l = USART1_TX_QUEUE_SIZE - 1;
        }
        
        {
            char buff[USART1_TX_QUEUE_SIZE];
            
            strncpy(buff, p, l);
            buff[l] = '\0';
            
            extern xQueueHandle UsartTxQueue;
            xQueueSend(UsartTxQueue, buff, portMAX_DELAY);
        }
        
        i -= l;
        p += l;
    }
    
    return(len);
}

xTaskHandle UsartTxHandler;
void vUsartTx(void *pvParameters) {
    while (1) {
        char buff[USART1_TX_QUEUE_SIZE];
        xQueueReceive(UsartTxQueue, buff, portMAX_DELAY);

        int l = strlen(buff);
        
        {
            extern uint8_t USART1_TX_Buff[USART1_TX_Buff_Size];
            memcpy(USART1_TX_Buff, buff, l);
        }
        
        DMA_SetCurrDataCounter(DMA1_Channel4, l);
        
        DMA_Cmd(DMA1_Channel4, ENABLE);
        USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
        
        vTaskSuspend(UsartTxHandler);
        
        USART_DMACmd(USART1, USART_DMAReq_Tx, DISABLE);
        DMA_Cmd(DMA1_Channel4, DISABLE);
    }
}

void DMA1_Channel4_IRQHandler(void) {
    DMA_ClearITPendingBit(DMA1_FLAG_TC4);
    xTaskResumeFromISR(UsartTxHandler);
}

送信はFreeRTOSのQueueを使用しているので、まず受け取ったデータをキューに入る大きさに分割するところから始める。受信したデータを破壊するのは心が痛むので、1ブロックずつバッファにコピーしてからキューに入れている。

vUsartTxではキューから受信を行う。キューに何もない場合は待たされるので、その間はCPUリソースを使用していない(と思う)。

キューから受け取った場合は文字列の長さを計測し、送信バッファにコピーして、DMAのデータ数を設定してからDMAを有効にする。DMAを有効にした後は送信が終わるまで待機する。
待機の手段としてサスペンドを利用しており、DMA転送終了割り込みの中でレジュームを行う。
レジュームするとその次から再開するので、DMAを無効にしてから振り出しに戻る。
この処理の中でstrlenを使用してることから分かる通り、この処理ではASCIIデータ以外を想定していない。バイナリデータを送ったりする場合は0x00が入ってるとそれ以降を送信しないので注意が必要。バイナリとかを送る可能性があるなら、キューに入れるバッファの先頭2バイトをデータサイズの変数にするとか工夫が必要。

説明を書いてて気がついたけど、vUsartTxでは一旦ローカルバッファに受け取ってからグローバルにコピーしてるけど、最初からグローバルに受け取っても良さそうだね。

あんまり参考にならないようなコードだけど、まぁこういう方法も有るんだなぁってことで。

0 件のコメント:

コメントを投稿