STM32G0 TIM BURST DMA DSHOT发波笔记

使用TIM1 CH1 发波DSHOT:




对于STM32G030F6P6定时器输入频率为64MHZ

设置分频值5 定时器计数频率为12.8MHz
定时器计数周期为20,定时器更新频率为0.64MHz,定时器更新周期为1.5625us

使用DSHOT600协议实现
中断和DMA配置:

DSHOT协议实现
定时器相关参数:

#define DSHOT_MOTOR_BIT_0       7
#define DSHOT_MOTOR_BIT_1       14
#define DSHOT_MOTOR_BITLENGTH   20

对于BIT1高电平使用14个定时器tick实现
对于BIT0高电平使用7个定时器tick实现
定时器计数周期为20

定时DSHOT相关参数:
首先是一个DSHOT数据帧的bit长度:

#define DSHOT_DMA_BUFFER_SIZE   18 /* resolution + frame reset (2us) */

正常来说一个DSHOT只含有16bit数据,但是因为使用TIM BURST DMA方法需要对帧尾进行复位,否则会持续发波,所以在尾部添加了两个bit,实际比较数值为0.

随后我们定义一个DSHOT BURST DMA缓存:

// 4个通道 DMA BURST BUFFER
uint32_t dmaBurstBuffer[DSHOT_DMA_BUFFER_SIZE * 4];//18x4 = 72 words

考虑到实际的使用情况,我们按照实际发波4个DSHOT通道的情况设计。
这里的传输大小可以使用uint16_t也可以使用uint32_t,但是实际上只要内存情况足够,最好还是使用uint32_t。
所以一个通道18字,一共4个通道总共72字的缓存区。

随后是DSHOT数据包的生成:

uint16_t prepareDshotPacket(const uint16_t value, bool requestTelemetry)
{
    uint16_t packet = (value << 1) | (requestTelemetry ? 1 : 0);
    // compute checksum
    int csum = 0;
    int csum_data = packet;
    for (int i = 0; i < 3; i++) {
        csum ^=  csum_data;   // xor data by nibbles
        csum_data >>= 4;
    }
    csum &= 0xf;
    packet = (packet << 4) | csum;// append checksum
    return packet;
}

这部分比较简单,也不是重点,具体可以参考DSHOT协议实现。
总之这个函数就是传入油门数值和遥测请求位,之后这个函数返回一个uint16_t表示16bit的DSHOT数据包。

随后就是需要根据数据包填充DMA缓存区:

void load_dma_buffer(uint32_t *dmaBuffer, int stride, uint16_t packet)//OK
{
    int i;
    for (i = 0; i < 16; i++) {// i: bit index
        dmaBuffer[i * stride] = (packet & 0x8000) ? DSHOT_MOTOR_BIT_1 : DSHOT_MOTOR_BIT_0;  // MSB first
        packet <<= 1;
    }
    dmaBuffer[i++ * stride] = 0;// 填充最后两个bit对应的buffer
    dmaBuffer[i++ * stride] = 0;// 填充最后两个bit对应的buffer
}

其中dmaBuffer并不是总和我们定义的72个字的那个地址一致。
这个load_dma_buffer函数只负责对于缓存区中一个通道的数值进行填充。
例如我们要填充第一个通道,则这个函数传入的地址将是:&(dmaBurstBuffer[0])
第二个通道:&(dmaBurstBuffer[1])
第三个通道: &(dmaBurstBuffer[2])
第四个通道:&(dmaBurstBuffer[3])
因为在这个函数中,我们传入stride的数值是4,表示每填充一个就跳过三个。
因为实际上DMA BURST传输的时候,DMA连续读取缓存中的内容,每次定时器更新就读取4个字,然后依次填充到CCR1 CCR2 CCR3 CCR4
这样一共填充18次,整个传输完毕。
而这样的一次填充4个字,其实就是填充4个DSHOT数据包的其中一个bit的比较值,需要总共18次,才可以填充完毕四个通道的整个DSHOT数据包。
而4个通道数据包的整个发送过程,仅仅需要触发一次中断,所以是对CPU十分友好的。

随后说明一下发送一个DSHOT帧的启动流程:
首先是定义一个函数:

// htim: htim1
// BurstBaseAddress: TIM_DMABASE_CCR1
// BurstRequestSrc: TIM_DMA_UPDATE
// BurstBuffer: buffer
// BurstLength: TIM_DMABURSTLENGTH_4TRANSFERS
// DataLength: 0-65535
HAL_StatusTypeDef dshot_dma_burst_start(TIM_HandleTypeDef *htim, uint32_t BurstBaseAddress,
                                        uint32_t BurstRequestSrc, const uint32_t *BurstBuffer,
                                        uint32_t  BurstLength,  uint32_t  DataLength)
{
    HAL_StatusTypeDef status = HAL_OK;
    if (htim->DMABurstState == HAL_DMA_BURST_STATE_BUSY){
        return HAL_BUSY;
    }
    else if (htim->DMABurstState == HAL_DMA_BURST_STATE_READY){
        if ((BurstBuffer == NULL) && (BurstLength > 0U)){
            return HAL_ERROR;
        }
        else{
            htim->DMABurstState = HAL_DMA_BURST_STATE_BUSY;
        }
    }
    else{
    }
    htim->hdma[TIM_DMA_ID_UPDATE]->XferCpltCallback = my_dma_cplt_callback;
    /* Enable the DMA channel */
    if (HAL_DMA_Start_IT(htim->hdma[TIM_DMA_ID_UPDATE], (uint32_t)BurstBuffer,
                        (uint32_t)&htim->Instance->DMAR, DataLength) != HAL_OK)
    {
    /* Return error status */
        return HAL_ERROR;
    }
    if (status == HAL_OK)
    {
        htim->Instance->DCR = (BurstBaseAddress | BurstLength);/* Configure the DMA Burst Mode */
        __HAL_TIM_ENABLE_DMA(htim, BurstRequestSrc); /* Enable the TIM DMA Request */
    }
    return status;
}

其实这个函数是从HAL库改过来的,主要是定义了一下回调函数,因为HAL库原来的回调函数不太好用。。。
调用这个函数表示启动一次DMA BURST传输,如果TIM出现DMA请求,则会立即开始传输。

对于定时器部分,我们只需要在系统启动的时候初始化一下时基单元和PWM通道输出即可:

void dshot_init(void)
{
    HAL_TIM_Base_Start(&DSHOT_TIMER_HANDLE);
    HAL_TIM_PWM_Start(&DSHOT_TIMER_HANDLE, TIM_CHANNEL_1);
}

注意这里的就是常规的启动方式,不需要IT或者DMA方式 Start。

在dshot_dma_burst_start中我们定义了回调函数:

htim->hdma[TIM_DMA_ID_UPDATE]->XferCpltCallback = my_dma_cplt_callback;

我们对这个函数进行实现:

void my_dma_cplt_callback(DMA_HandleTypeDef *hdma)
{
    TIM_HandleTypeDef *htim = (TIM_HandleTypeDef *)((DMA_HandleTypeDef *)hdma)->Parent;
    if (htim->hdma[TIM_DMA_ID_UPDATE]->Init.Mode == DMA_NORMAL)
    {
        htim->State = HAL_TIM_STATE_READY;
    }
    HAL_TIM_PeriodElapsedCallback(htim);
}

其中调用了HAL库为我们定义的weak函数:

// DMA BURST 传输回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    HAL_TIM_DMABurst_WriteStop(&DSHOT_TIMER_HANDLE, TIM_DMA_UPDATE);// 不加这个会导致BURST BUSY
}

主要内容就是停止了BURST传输,否则在下一次传输启动的时候会出现BURST BUSY。

对于stm32g0xx_it.c里面的函数,不需要进行修改。

posted @ 2025-05-14 14:15  LGQ_Wakkk  阅读(297)  评论(0)    收藏  举报