编码器数据程序分析
固件版本 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_pin和 get_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;