在上一节中我们调用STM32 MCSDK的API获取了观测器,测量得到2208电机的转速值,并同AS5600编码器返回的速度值进行比对,验证了观测器在中高速运行状态下的灵敏度和准确性。这一节我们将使用板载电位器,尝试完成速度调节功能,并将目标速度显示到ST7735 LCD屏幕上。
工程创建、电机参数的测定、屏幕驱动和硬件连接在前面几篇文章都已经提及,此处不再赘述。

1.引脚分配
查看IHM16M1开发板原理图,可以发现电位器SPEED网络连接到STM32G431的PC2引脚,因此我们在CubeMX中将PC2配置为ADC1_IN8通道,并设置ADC1_IN8为单端输入模式。
因为ADC1同样用于电机母线电压、电流等等的测量,故最好不要修改ADC已经配置好的参数,直接使用即可。


2.MCSDK API介绍
MCSDK的API在mc_api.h/.c中提供。下面介绍的是本文中将调用的接口。
①启停接口:
bool MC_StartMotor1 (void)
启动电机1的启动程序。
bool MC_StopMotor1 (void)
启动电机1的停止程序。
②状态接口:
int16_t MC_GetMecSpeedReferenceMotor1 (void)
返回电机1的当前机械转子转速参考,以SPEED_UNIT定义的单位表示。
关于SPEED_UNIT有如下解释:
转速单位用于与应用程序的接口。
这个符号定义了API函数用于其速度参数的单位中1赫兹的值。
例如,如果选择的单位是RPM,则SPEED_UNIT被定义为60,因为1 Hz等于60 RPM。默认的单位是U_01HZ,由Workbench在项目的初始生成中设置。由于此符号是在用户节中定义的,因此用户设置的自定义值将在整个项目再生过程中持续存在。
电机控制工作台计算的调速PID参数适用于01Hz的转速。电机控制子系统内部缩放它们以适应实际的速度单位。
此符号不应设置为文字数值。相反,应该将其设置为为此目的预定义的符号之一,例如U_RPM, U_01HZ,…有关详细信息,请参阅SpeedUnit。
int16_t MC_GetMecSpeedAverageMotor1 (void)
返回电机1最后一次计算的平均机械转子转速,用SPEED_UNIT定义的单位表示。
MCI_State_t MC_GetSTMStateMotor1 (void)
返回电机1状态机的当前状态。
uint16_t MC_GetCurrentFaultsMotor1 (void)
返回一个位字段,显示电机1上的所有当前故障。
③修改速度参数接口:
void MC_ProgramSpeedRampMotor1 (int16_t hFinalSpeed, uint16_t hDurationms)
修改电机1的目标速度。
如果电机1的状态机处于RUN状态,则立即执行。可以使用MC_GetCommandStateMotor1() 检查命令的状态。如果应用程序使用无传感器电机控制技术,则不能反转旋转方向。如果hFinalSpeed参数的符号与当前速度的符号不同,则当转速即将达到0rpm时,将不会完成速度斜坡并发生速度反馈错误(MC_SPEED_FDBK)。
其他的函数接口可以自行查阅。
3.程序分析及编写
①原理分析
STM32G431的ADC模数转换器将模拟信号转换为12位数字值,范围是0~4095。在MCSDK的FOC框架中,ADC1和ADC2被主控制器占用,其高频任务通过注入通道实时采样三相电流信号,用于驱动电流反馈控制环。
最开始编写代码直接调用HAL原生函数,每 20ms 直接改ADC1通道去读电位器,导致和MCSDK的ADC任务抢资源,启动过后卡住并直接报错。为了避免这类资源竞争,MCSDK提供了RCM(Regular Conversion Manager)组件来允许用户访问ADC。RCM通过注册机制让用户代码提交转换请求,然后在FOC的高频任务中,利用FOC的安全时间窗口来执行这些常规转换,电位器读取就能在不破坏FOC运行的前提下安全进行。改用RCM而不是 HAL_ADC_ConfigChannel() 之后,电位器读取功能不再抢占FOC任务的资源,程序顺利运行。
我们可以查看RCM的头文件regular_conversion_manager.h的内容:
一个结构体定义,包含了执行转换时需要的所有参数,注册ADC通道时需要先定义好一个 RegConv_t:
typedef struct
{
ADC_TypeDef *regADC; /*!< ADC peripheral used for the conversion */
uint32_t samplingTime; /*!< ADC sampling time used for the conversion */
uint8_t channel; /*!< ADC channel used for the conversion */
uint16_t data; /*!< ADC converted value */
uint8_t id; /*!< index of the conversion in RCM array */
} RegConv_t;
typedef void (*RCM_exec_cb_t)(RegConv_t *regConv, uint16_t data, void *UserData);
用于注册规则转换的函数:
bool RCM_RegisterRegConv(RegConv_t *regConv);
用于在 HF 任务内部启动转换的非阻塞函数:
void RCM_ExecNextConv(void);
用于回读已经开始的规则转换的非阻塞函数:
void RCM_ReadOngoingConv(void);
用于执行已经注册的规则转换的函数:
uint16_t RCM_ExecRegularConv(RegConv_t *regConv);
用于读取存储在数据结构中的规则转换结果的函数:
static inline uint16_t RCM_GetRegularConv(const RegConv_t *regConv)
{
#ifdef NULL_PTR_CHECK_REG_CON_MNG
return ((MC_NULL == regConv) ? 0U : regConv->data);
#else
return (regConv->data);
#endif
}
用于等待规则转换结果的函数:
void RCM_WaitForConv(void);
因为FOC任务中已经有规律地执行RCM转换,所以以上接口只需要调用 RCM_RegisterRegConv(RegConv_t regConv) 注册RCM,以及 RCM_GetRegularConv(const RegConv_t regConv) 读取RCM的结果即可。
注意到ADC1数据设置为左对齐,在计算的时候应该右移4位,否则计算得到的数据会是实际值的4倍。

②程序编写
定义以下变量以及结构体:
//ADC采集及目标速度计算的定时
static uint32_t pot_update_time = 0;
static const uint32_t POT_UPDATE_INTERVAL = 20; // 20ms
static uint16_t pot_raw_latest = 0xFFFF; //原始的16位数据
static int16_t pot_target_rpm_latest = 0; //计算后的速度值
static bool pot_regconv_registered = false; //注册RCM的返回值,bool类型
//用于注册RCM的结构体
static RegConv_t PotRegConv =
{
.regADC = ADC1,
.channel = MC_ADC_CHANNEL_8,
.samplingTime = LL_ADC_SAMPLINGTIME_47CYCLES_5,
.data = 0U
};
在显示刷新函数中加入目标速度的刷新,同样,当检测到目标速度发生改变时才刷新显示:
static void Display_UpdateMotorInfo(void)
{
//……
//显示上加入目标速度
if (pot_target_rpm_latest != last_target_rpm)
{
snprintf(line_buf, sizeof(line_buf), "%4d rpm", pot_target_rpm_latest);
ST7735_ShowString(56, 88, BLACK, WHITE, line_buf);
last_target_rpm = (uint16_t)pot_target_rpm_latest;
}
}
速度更新函数,主要负责读取ADC1_IN8、计算ADC映射的速度值,并通过 MC_ProgramSpeedRampMotor1_F()接口将浮点数速度值更新到电机1的状态机中。
加入了保护机制:电机状态不为RUN的时候不更新速度曲线的终值,RCM未注册的时候也不更新速度曲线的终值。
计算方面,因为ADC的12位数据设置为左对齐,所以需要先右移4位进行右对齐;将0~4095的计数值映射到0~最大速度值,最大速度调用pmsm_motor_parameter.h里的MOTOR_MAX_SPEED_RPM宏定义,保证与MCSDK的设置统一。
最后调用MC_ProgramSpeedRampMotor1_F((float_t)target_rpm, 50U)上传速度值,加减速时间设定为50ms。
static void Pot_UpdateSpeedCommand(void)
{
MCI_State_t motor_state = MC_GetSTMStateMotor1();
/* Avoid buffering speed ramps before RUN, otherwise startup target can be overridden to 0 rpm. */
if (motor_state != RUN)
{
last_pot_target_rpm = INT16_MIN;
return;
}
if (!pot_regconv_registered)
{
return;
}
uint16_t pot_raw = RCM_GetRegularConv(&PotRegConv);
uint16_t pot_raw_12b = (uint16_t)(pot_raw >> 4);
/* ADC is configured as left-aligned 12-bit, convert back to 0..4095 first. */
/* Map 0..4095 to 0..MOTOR_MAX_SPEED_RPM */
int16_t target_rpm = (int16_t)(((uint32_t)pot_raw_12b * MOTOR_MAX_SPEED_RPM) / 4095U);
pot_raw_latest = pot_raw;
pot_target_rpm_latest = target_rpm;
if (target_rpm != last_pot_target_rpm)
{
MC_ProgramSpeedRampMotor1_F((float_t)target_rpm, 50U);
last_pot_target_rpm = target_rpm;
}
}
在主函数的初始化阶段,需要先完成RCM的注册,保证电位器的正常使用:
pot_regconv_registered = RCM_RegisterRegConv(&PotRegConv);
在while循环中需要定时调用Pot_UpdateSpeedCommand()任务:
while (1)
{
if (HAL_GetTick() - encoder_update_time >= ENCODER_UPDATE_INTERVAL)
{
Encoder_UpdateFromAS5600();
encoder_update_time = HAL_GetTick();
}
if (HAL_GetTick() - pot_update_time >= POT_UPDATE_INTERVAL)
{
Pot_UpdateSpeedCommand();
pot_update_time = HAL_GetTick();
}
if (lcd_init_done && (HAL_GetTick() - display_update_time >= DISPLAY_UPDATE_INTERVAL))
{
Display_UpdateMotorInfo();
display_update_time = HAL_GetTick();
}
}
代码已经上传到Github上:Github仓库:https://github.com/Chiando-1100/IHM16M1_Display