ODrive0.5.1程序分析#5 编码器数据采集(spi通信采集绝对值编码器as5047)

编码器数据程序分析

固件版本 fw-0.5.1(0积分下载)

代码分析

1.初始化

首先在mian.cpp文件中,包含odrive_main()函数,在encoder_.setup() 进行设置spi通讯,以采集编码器信息

int odrive_main(void) {
...
    for(auto& axis : axes){
        axis->encoder_.setup();
...
    }
}

encoder_.setup() 函数中,HAL_TIM_Encoder_Start() 为启动STM32的定时器编码器模式, TIM_CHANNEL_ALL 为编码器模式下使用的通道,all表示通道1,2均被使能。timer为指向结构体TIM_HandleTypeDef的指针,该函数成功时会返回HAL_OK
如果你的编码器带有索引信号(Z),则需要进行 set_idx_subscribe() ,对 索引信号的GPIO进行使能,这里使用的as5047的spi通讯,不包含索引信号,因此不对索引信号的GPIO使能。
mode_ 为在上位机中配置的编码器模式,这里为 MODE_SPI_ABS_AMS,当编码器模式配为MODE_INCREMENTAL增量式时,mode_=0,故if的判断条件为FALSE,不进行下列初始化。
abs_spi_cs_pin_init() 为对 cs片选引脚GPIO进行初始化,见下方代码;
abs_spi_init() 为对 spi通讯进行配置初始化,见下方代码。

void Encoder::setup() {
    HAL_TIM_Encoder_Start(hw_config_.timer, TIM_CHANNEL_ALL);
    set_idx_subscribe();

    mode_ = config_.mode;
    if(mode_ & MODE_FLAG_ABS){
        abs_spi_cs_pin_init();
        abs_spi_init();
        if (axis_->controller_.config_.anticogging.pre_calibrated) {
            axis_->controller_.anticogging_valid_ = true;
        }
    }
}

abs_spi_cs_pin_init() 函数为片选引脚GPIO初始化,首先获取片选信号的端口号和引脚号,利用函数get_gpio_port_by_pinget_gpio_pin_by_pin,原理是通过在上位机输入的片选引脚对应的驱动板插口号码,不同号码对应不同的GPIO口,然后获取对应的端口号和引脚号。
之后进行GPIO初始化,模式为推挽输出模式,带上拉,保证在默认状态下为高电平,因为spi通信在cs引脚在低电平时开始工作。最后对cs引脚写1。

void Encoder::abs_spi_cs_pin_init(){
    // Decode cs pin
    abs_spi_cs_port_ = get_gpio_port_by_pin(config_.abs_spi_cs_gpio_pin);
    abs_spi_cs_pin_ = get_gpio_pin_by_pin(config_.abs_spi_cs_gpio_pin);

    // Init cs pin
    HAL_GPIO_DeInit(abs_spi_cs_port_, abs_spi_cs_pin_);
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.Pin = abs_spi_cs_pin_;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(abs_spi_cs_port_, &GPIO_InitStruct);

    // Write pin high
    HAL_GPIO_WritePin(abs_spi_cs_port_, abs_spi_cs_pin_, GPIO_PIN_SET);
}

spi通讯配置初始化
配置依次为:驱动器的SPI模式主机模式全双工数据模式;数据大小16bit空闲低电平也即CPOL=0;在偶数边沿采样,也即CPHS=1;(故模式为下降沿采样);片选由软件管理;波特率设置32分频;设置数据为高位先行;SPI_TImode不使能(目前常用的SPI接口,标准是摩托罗拉制定的,当时TI还有另一套标准,就是所谓的SSP,也就是现在见到的SPI TImode。这个模式也是主从双方都必须同时使用,否则会出现乱码);CRC校验不使能;然后先利用HAL_SPI_DeInit函数进行反初始化,关闭外设,所有寄存器恢复默认值,方便后面调用初始化函数HAL_SPI_Init进行初始化。(该部分在spi.c中已经进行了初始化)
HAL_SPI_Init()函数中包含了HAL_SPI_MspInit函数,该函数在spi.c进行定义,用于初始化MISO,MOSI,SCK引脚的GPIO以及初始化DMA通道。

bool Encoder::abs_spi_init(){
    if ((mode_ & MODE_FLAG_ABS) == 0x0)
        return false;

    SPI_HandleTypeDef * spi = hw_config_.spi;
    spi->Init.Mode = SPI_MODE_MASTER;
    spi->Init.Direction = SPI_DIRECTION_2LINES;
    spi->Init.DataSize = SPI_DATASIZE_16BIT;
    spi->Init.CLKPolarity = SPI_POLARITY_LOW;
    spi->Init.CLKPhase = SPI_PHASE_2EDGE;
    spi->Init.NSS = SPI_NSS_SOFT;
    spi->Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32;
    spi->Init.FirstBit = SPI_FIRSTBIT_MSB;
    spi->Init.TIMode = SPI_TIMODE_DISABLE;
    spi->Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    spi->Init.CRCPolynomial = 10;
    if (mode_ == MODE_SPI_ABS_AEAT) {
        spi->Init.CLKPolarity = SPI_POLARITY_HIGH;
    }
    HAL_SPI_DeInit(spi);
    HAL_SPI_Init(spi);
    return true;
}

首先进行GPIO初始化,SCK,MISO,MOSI分别对应PC10,PC11,PC12,模式都选择复用输出功能带上拉(因为在接收数据时MISO始终为低电平,可以用来检测编码器是否断开),复用功能选择SPI3
接着对SPI发送的DMA进行初始化,通道选择数据流5的通道0(在STM32F40x的数据手册上可以查找到),在采集编码器数据时,主机SPI的发送为对MOSI引脚始终发送固定格式数据,故传输方向选择存储器到外设,外设地址不递增,内存地址递增,再根据SPI数据大小设置DMA传输数据大小,DMA采集模式选择DMA_NORMAL不进行循环采集,优先级中级。调用函数写入配置 。
最后对SPI接收的DMA进行初始化,在采集时为在主机MISO引脚进行接收数据,故方向为外设到储存器,其他配置相同。

void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct;
  if(spiHandle->Instance==SPI3)
  {
  /* USER CODE BEGIN SPI3_MspInit 0 */

  /* USER CODE END SPI3_MspInit 0 */
    /* SPI3 clock enable */
    __HAL_RCC_SPI3_CLK_ENABLE();
  
    /**SPI3 GPIO Configuration    
    PC10     ------> SPI3_SCK
    PC11     ------> SPI3_MISO
    PC12     ------> SPI3_MOSI 
    */
    GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_PULLUP; // required for disconnect detection on SPI encoders
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF6_SPI3;
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

    /* SPI3 DMA Init */
    /* SPI3_TX Init */
    hdma_spi3_tx.Instance = DMA1_Stream5;
    hdma_spi3_tx.Init.Channel = DMA_CHANNEL_0;
    hdma_spi3_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_spi3_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_spi3_tx.Init.MemInc = DMA_MINC_ENABLE;

    if(spiHandle->Init.DataSize == SPI_DATASIZE_8BIT){
      hdma_spi3_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
      hdma_spi3_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    } else {
      hdma_spi3_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
      hdma_spi3_tx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;      
    }
    
    hdma_spi3_tx.Init.Mode = DMA_NORMAL;
    hdma_spi3_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
    hdma_spi3_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    if (HAL_DMA_Init(&hdma_spi3_tx) != HAL_OK)
    {
      _Error_Handler(__FILE__, __LINE__);
    }

    __HAL_LINKDMA(spiHandle,hdmatx,hdma_spi3_tx);

    /* SPI3_RX Init */
    hdma_spi3_rx.Instance = DMA1_Stream0;
    hdma_spi3_rx.Init.Channel = DMA_CHANNEL_0;
    hdma_spi3_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_spi3_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_spi3_rx.Init.MemInc = DMA_MINC_ENABLE;
    if (spiHandle->Init.DataSize == SPI_DATASIZE_8BIT) {
        hdma_spi3_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
        hdma_spi3_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    } else {
        hdma_spi3_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
        hdma_spi3_rx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    }
    hdma_spi3_rx.Init.Mode = DMA_NORMAL;
    hdma_spi3_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
    hdma_spi3_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    if (HAL_DMA_Init(&hdma_spi3_rx) != HAL_OK)
    {
      _Error_Handler(__FILE__, __LINE__);
    }

    __HAL_LINKDMA(spiHandle,hdmarx,hdma_spi3_rx);

    /* SPI3 interrupt Init */
    HAL_NVIC_SetPriority(SPI3_IRQn, 5, 0);
    HAL_NVIC_EnableIRQ(SPI3_IRQn);

  }
}
2.根据时序启动采集

在ADC2和3的中断中,pwm_trig_cb() 为回调函数,进入ADC_IRQ_Dispatch() 函数。

void ADC_IRQHandler(void)
{
  /* USER CODE BEGIN ADC_IRQn 0 */

  // The HAL's ADC handling mechanism adds many clock cycles of overhead
  // So we bypass it and handle the logic ourselves.
  //@TODO add vbus measurement on adc1 here
  ADC_IRQ_Dispatch(&hadc1, &vbus_sense_adc_cb);
  ADC_IRQ_Dispatch(&hadc2, &pwm_trig_adc_cb);
  ADC_IRQ_Dispatch(&hadc3, &pwm_trig_adc_cb);
  ...}

传入的pwm_trig_cb() 为回调函数,JE0C为检查注入组转化完成标志ADC_FLAG_JEOC(ADC的SR寄存器JEOC位),JEOC_IT_EN为检查注入组转化完成后中断源是否启用ADC_IT_JEOC(ADC的CR1寄存器JEOCIE位);
当注入组转化完成且中断启用时,回调函数pwm_trig_adc_cb中传入参数inject置True
同理:EOC为检查规则组转化完成标志,EOC_IT_EN为检查规则组转化完成后的中断源是否启用。
当规则组组转化完成且中断启用时,回调函数pwm_trig_adc_cb中传入参数inject置False

void ADC_IRQ_Dispatch(ADC_HandleTypeDef* hadc, ADC_handler_t callback) {

  // Injected measurements
  uint32_t JEOC = __HAL_ADC_GET_FLAG(hadc, ADC_FLAG_JEOC);
  uint32_t JEOC_IT_EN = __HAL_ADC_GET_IT_SOURCE(hadc, ADC_IT_JEOC);
  if (JEOC && JEOC_IT_EN) {
    callback(hadc, true);
    __HAL_ADC_CLEAR_FLAG(hadc, (ADC_FLAG_JSTRT | ADC_FLAG_JEOC));
  }
  // Regular measurements
  uint32_t EOC = __HAL_ADC_GET_FLAG(hadc, ADC_FLAG_EOC);
  uint32_t EOC_IT_EN = __HAL_ADC_GET_IT_SOURCE(hadc, ADC_IT_EOC);
  if (EOC && EOC_IT_EN) {
    callback(hadc, false);
    __HAL_ADC_CLEAR_FLAG(hadc, (ADC_FLAG_STRT | ADC_FLAG_EOC));
  }
}

在回调函数中,找到关于spi的部分:
首先通过上步传入参数True/False选择axis_num电机0还是电机1(ODrive一块驱动器可以带两个电机)。
counting_down是对定时器计数器方向做判定,CR1寄存器的DIR位向上计数位1,向下计数为0,current_meas_not_DC_CAL等于非counting_down,也就是向上计数时该值为0,向下计数时该值为1;
当为电机0向下计数时,执行if内程序;当为电机1向上计数时执行if内程序(目的是为了防止电机0与电机1的SPI通讯发生冲突)。
if函数内执行函数axis.encoder_.abs_spi_start_transaction(),进行启动SPI的DMA传输通道进行接收数据。

void pwm_trig_adc_cb(ADC_HandleTypeDef* hadc, bool injected) {
...
    int axis_num = injected ? 0 : 1;
    bool counting_down = axis.motor_.hw_config_.timer->Instance->CR1 & TIM_CR1_DIR;
    bool current_meas_not_DC_CAL = !counting_down;

        if((current_meas_not_DC_CAL && !axis_num) ||
                (axis_num && !current_meas_not_DC_CAL)){
            axis.encoder_.abs_spi_start_transaction();
        }
...
    }
3.采集数据

对于abs_spi_start_transaction()函数
mode_ 为编码器类型选择,有 INCREMENTAL=0,HALL=1,SINCOS=2,SPI_ABS_CUI=256,SPI_ABS_AMS=257,SPI_ABS_AEAT=258; 对于 MODE_FLAG_ABS该值为静态变量
0X100
也就是256,故只有在编码器类型的值mode≥256时,可执行if内代码,我们使用的绝对值编码器AS5047,类型选择SPI_ABS_AMS,故可以执行下面程序。
检查外设SPI状态,不为READY时,报错返回false;
SPI状态无误,执行**HAL_GPIO_WritePin()**函数,该函数作用为拉低cs引脚,因为片选引脚为低电平时SPI开始通信。
HAL_SPI_TransmitReceive_DMA() 函数为利用DMA传输和接收大量数据,在该函数中通过检测SPI状态,通过 HAL_DMA_Start_IT() 使能SPI接收和发送的DMA的流,通过DMA进行传输和接收数据。

bool Encoder::abs_spi_start_transaction(){
    if (mode_ & MODE_FLAG_ABS){
        axis_->motor_.log_timing(TIMING_LOG_SPI_START);
        if(hw_config_.spi->State != HAL_SPI_STATE_READY){
            set_error(ERROR_ABS_SPI_NOT_READY);
            return false;
        }
        HAL_GPIO_WritePin(abs_spi_cs_port_, abs_spi_cs_pin_, GPIO_PIN_RESET);
        HAL_SPI_TransmitReceive_DMA(hw_config_.spi, (uint8_t*)abs_spi_dma_tx_, (uint8_t*)abs_spi_dma_rx_, 1);
    }
    return true;
}

分析**HAL_SPI_TransmitReceive_DMA()**函数输入:首先时SPI号,发送数据,以及接收数据地址,数据长度。
发送地址查询AS5047数据手册,figure13命令格式说明14bit在读取数据时为1,bit13:0为地址,在figure18中说明读取带补偿的角度地址为0X3FFF,也就是(0011 1111 1111 1111),由于为读故14bit也为1, 15bit进行偶数补偿故15bit也为1,故发送数据为0XFFFF。

在这里插入图片描述
通过DMA接收的数据 abs_spi_dma_rx_ 已经包含了位置信息,在encoder.cpp中进行赋值并于0X3FFF相与,提取位置信息,完成对编码器数据的获取。

        case MODE_SPI_ABS_AMS: {
            uint16_t rawVal = abs_spi_dma_rx_[0];
            // check if parity is correct (even) and error flag clear
            if (ams_parity(rawVal) || ((rawVal >> 14) & 1)) {
                return;
            }
            pos = rawVal & 0x3fff;
        } break;

本部分结束

原创不易,如果对你有帮助的话,希望可以点赞关注收藏,谢谢😊
  • 11
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值