2016年9月3日土曜日

STM32F1で多チャンネルのPWMを生成する

STM32F1でPWMを出そうとすれば、大抵の場合はTIMのOCを使用すると思います。これはかなり高精度で、CPUやバスを使用せずに、簡単にPWMを生成できます。ただし弱点はTIM1個あたり4本しか出せない点で、10本欲しいという時はTIMが3個必要になります。TIMは様々な用途に使用しますから、PWMだけにTIMを大量に割り当てるわけにも行きません。
というわけでなにか違う方法が必要となります。

今回はGPIOを使用してPWMを出力してみました。
と言ってもCPUで一々1bitずつ操作していてはタイミングも保証できませんし、CPUリソースの1割を使用してしまうので割に合いません。なので今回はDMAを使用します。

GPIOで任意のデータを吐き出す場合、ODRに書き出すことも可能ですが、パルス幅を変更するたびに大量のビット操作が必要となるため、かなりCPUリソースを使用することが予想されます。ということでSTMのGPIOに搭載されているBSRRというレジスタを使用します。
このBSRRはport Bit Set/Reset Registerの略で、32bitレジスタ1本の下位16bitが1になっているビットはSetされ、上位16bitが1になっているビットはResetされるというレジスタです。StdPeriphLibraryのGPIOビット操作関数はBSRRに引数を投げているだけです。
BSRRを使用してパルス幅を変更したい場合、変更する動作は数回のビット操作で済むので、非常に高速に行えるようになります。

今回はGPIOEの16bitをPWM出力ポートとし、DMA1-1を使用してBSRRに転送、DMAのトリガはTIM4を1MHzで回す、という感じにしました。
DMA1-1を使う理由は、それ以外のDMAを使用した場合、調停負けを起こした場合にパルス幅の正確性を維持できないという理由になります。DMAの調停は4段階の優先度(VeryHigh, High, Medium, Low)で決定され、同時に同じ優先度の転送が発生した場合はチャンネル数の小さい方が優先されます。


今回の方法の利点は上記の通り簡単にパルス幅を設定できたり、比較的高精度にパルスを出力できる点です。
ただ欠点もいくつかあり、バスを高頻度で使用する、大量のメモリが必要、といった点になります。特に後者は深刻で、最大2msecのパルスを出力する場合、およそ8.5kbyteのメモリが必要となります。STBeeは64kのRAMがあるので多少はマシですが、STBeeMiniの場合は20kしかRAMがありませんから、STBeeMiniで複雑な処理を行いながら大量のサーボを制御したいという場合は注意が必要です。


他の方法としては、TLC5940を始めとした、外付けのチップを使用する事も可能です。例えばTLC5940は1個あたり16chを制御でき、カスケード接続すれば64位は簡単に制御できるはずです。これくらいあれば8足歩行ロボットでも作らないかぎりは安心でしょう。ただTLC5940であればSPIやPWM出力、GPIO等が必要になり、プログラムも複雑になりますし、外付け部品が必要になるので難易度は若干上昇します。


とりあえず今回作ったプログラムの一部を以下に貼っておきます。これはFreeRTOSの上で動くモノですが、DMAの操作(Disable, SetCounter, Enable)をタイマ割り込みで行ったりするようにすれば非OS環境でも動作するはずです。


GPIOEをロジアナで覗くと以下のようになります(ジャンパワイヤが足りないので5ch分だけ)。


わかりやすいように100、200、300、400、1500usecとなっていますが、サーボモータを接続する場合は1500±500usecを使用します。



uint32_t PwmBuff[2100];
const int PwmBuffSize = sizeof(PwmBuff) / sizeof(PwmBuff[0]);
uint16_t PwmWidths[16];

void SetPwmWidth(uint8_t ch, uint16_t width) {
    if (ch >= 16) {
        return;
    }

    if (width >= PwmBuffSize) {
        return;
    }

    uint32_t mask = 0x00010000 << ch;

    PwmBuff[PwmWidths[ch]] &= ~mask;
    PwmWidths[ch] = width;
    PwmBuff[PwmWidths[ch]] |= mask;
}

void vPWMTx(void *pvParameters) {
    {
        GPIO_InitTypeDef GPIO_InitStructure;

        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);

        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
        GPIO_Init(GPIOE, &GPIO_InitStructure);
    }

    {
        TIM_TimeBaseInitTypeDef TIM_InitStructure;

        RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);

        TIM_InitStructure.TIM_Period = 72 - 1;
        TIM_InitStructure.TIM_Prescaler = 0;
        TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
        TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up;
        TIM_InitStructure.TIM_RepetitionCounter = 0;

        TIM_TimeBaseInit(TIM4, &TIM_InitStructure);
    }

    {
        TIM_OCInitTypeDef TIM_OCInitStructure;

        TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
        TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Disable;
        TIM_OCInitStructure.TIM_Pulse = 1;
        TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;

        TIM_OC1Init(TIM4, &TIM_OCInitStructure);

        TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);
    }

    TIM_ARRPreloadConfig(TIM4, ENABLE);

    {
        RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

        DMA_InitTypeDef DMA_InitStructure;

        DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&GPIOE->BSRR);
        DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)(&PwmBuff[0]);
        DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
        DMA_InitStructure.DMA_BufferSize = PwmBuffSize;
        DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
        DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
        DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
        DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
        DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
        DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
        DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

        DMA_Init(DMA1_Channel1, &DMA_InitStructure);

        TIM_DMACmd(TIM4, TIM_DMA_CC1, ENABLE);
    }

    {
        memset(PwmBuff, 0, sizeof(PwmBuff));
        memset(PwmWidths, 0, sizeof(PwmWidths));

        PwmBuff[0] = 0x0000FFFF;

        SetPwmWidth(0, 100 * 1);
        SetPwmWidth(1, 100 * 2);
        SetPwmWidth(2, 100 * 3);
        SetPwmWidth(3, 100 * 4);
        SetPwmWidth(4, 100 * 5);
        SetPwmWidth(5, 100 * 6);
        SetPwmWidth(6, 100 * 7);
        SetPwmWidth(7, 100 * 8);
        SetPwmWidth(8, 100 * 9);
        SetPwmWidth(9, 100 * 11);
        SetPwmWidth(10, 100 * 11);
        SetPwmWidth(11, 100 * 12);
        SetPwmWidth(12, 100 * 13);
        SetPwmWidth(13, 100 * 14);
        SetPwmWidth(14, 100 * 15);
        SetPwmWidth(15, 100 * 16);
    }

    TIM_Cmd(TIM4, ENABLE);
    
    while (1) {
        DMA_Cmd(DMA1_Channel1, DISABLE);
        DMA_SetCurrDataCounter(DMA1_Channel1, PwmBuffSize);
        DMA_Cmd(DMA1_Channel1, ENABLE);

        vTaskDelay(20 / portTICK_RATE_MS);
    }
}

0 件のコメント:

コメントを投稿