最近在学习无刷电机的FOC控制,硬件平台是STM32F103C8T6,MT6701磁编码器(SPI接口),三相全桥驱动,双ADC注入组采样电流,上位机用VOFA+看波形。
程序架构参考了一个开源例程:ADC中断里执行FOC核心算法(Clarke、Park、PID、SVPWM),主循环只做printf和LED闪烁。
按照例程移植完之后,遇到了两个问题,在这里记录一下排查过程。
问题一:串口发不出数据
现象
程序烧录后,LED正常闪烁,说明主循环在跑。但VOFA+接收区没有任何数据。试了`printf`和直接调用`HAL_UART_Transmit`,都收不到。
先检查了软件层面:
`fputc`函数已重定向到USART2
Keil里`Use MicroLIB`已勾选
波特率115200,COM口选的COM3
硬件TX/RX没接反,GND也共地了
这些看起来都没问题。
然后我把`while(1)`里改成只发一句最裸的测试:
HAL_UART_Transmit(&huart2, (uint8_t*)"HELLO\r\n", 7, 100);
HAL_Delay(500);
结果还是收不到。
我做了什么测试
把商家提供的能正常运行的例程和自己的工程做了逐文件对比:
`usart.c`里的`MX_USART2_UART_Init`函数——配置完全一样
`main.c`里的`SystemClock_Config`——看起来也一样
但仔细看`SystemClock_Config`的最后一行
PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV2
例程里是`RCC_ADCPCLK2_DIV6`。
打开CubeMX的Clock Configuration看了一下,ADC预分频选的是/2,ADC时钟显示36MHz,而且是红色的
查到的资料
翻STM32F103的数据手册,ADC时钟的最大值是14MHz。36MHz超了2.5倍还多。
根因
ADC时钟超频导致外设时序紊乱,USART2的发送被干扰了。
解决
在CubeMX里把ADC Prescaler从/2改成/6,ADC时钟变成12MHz。重新生成代码,编译烧录,串口立刻正常了。
小结
CubeMX里红色的配置项不能忽略,它是在提示你配置不合法。ADC超频这种问题,表面上看和串口无关,但实际上会影响整个芯片的外设时序。
问题二:程序卡死,电机发
现象
串口通了之后,把电流模式加进`while(1)`,结果程序卡在这行之后
motor_control_context.type = control_type_torque;
后面的printf不打印了。同时电机开始发烫——PWM还在输出,但FOC中断似乎没有更新占空比。
我做了什么测试
在`case control_type_torque:`前后加printf:
case control_type_torque:
printf("T-start\r\n");
lib_torque_control(motor_control_context.torque_norm_d, motor_control_context.torque_norm_q);
printf("T-end\r\n");
break;
结果:`T-start`打印了,`T-end`没打印。说明卡在`lib_torque_control`里。
继续往里加:
void lib_torque_control(float torque_norm_d, float torque_norm_q)
{
printf("L1\r\n");
float d = torque_d_loop(torque_norm_d);
printf("L2\r\n");
float q = torque_q_loop(torque_norm_q);
printf("L3\r\n");
foc_forward(d, q, rotor_logic_angle);
printf("L4\r\n");
}
结果打印了`L1`、`L2`、`L3`,`L4`没打印。说明卡在`foc_forward`。
再往里:
void foc_forward(float d, float q, float rotor_rad)
{
printf("F1\r\n");
float d_u = 0, d_v = 0, d_w = 0;
svpwm(rotor_rad, d, q, &d_u, &d_v, &d_w);
printf("F2\r\n");
set_pwm_duty(d_u, d_v, d_w);
printf("F3\r\n");
}
结果打印了4个`F1`(进入函数、三个变量初始化、`svpwm`完成),第5个`F1`(set_pwm_duty之后)没打印。
卡在`set_pwm_duty`函数里。
查到的资料
打开`foc.c`,发现里面有一个weak版本的`set_pwm_duty`:
void set_pwm_duty(float d_u, float d_v, float d_w) __attribute__((weak));
void set_pwm_duty(float d_u, float d_v, float d_w)
{
while (1)
;
}
而我的`main.c`里也有一个同名的`set_pwm_duty`,是真正写寄存器的版本。
我查了一下`__attribute__((weak))`的机制:它表示这是一个“弱符号”,如果链接时发现别处有强符号(普通函数),就用强符号。如果没有,才用这个弱符号。
按理说我的main.c里有强符号,链接器应该用main.c的版本。但实际上,链接器选了foc.c里的weak版本,导致程序卡在while(1)。
关于weak,我的理解
作者在foc.c里放一个weak版本的set_pwm_duty,我理解是为了解耦:
foc.c只负责FOC的数学计算,不应该关心底层硬件是哪个芯片、哪个定时器。
它只需要知道调用set_pwm_duty就能输出PWM就够了。
所以foc.c里放一个weak版本的占位函数,如果你的工程里有实现,就用你的;如果没有实现,就执行这个占位函数。
占位函数写成`while(1);`是故意的:如果忘了实现底层驱动,程序会卡死在这里,让你立刻发现。** 如果占位函数是空的,程序会静默地不输出PWM,电机不转,但你不知道是哪里出了问题,更难查。
这个设计思路是对的,但在我这里翻车了——链接器选了weak版本而不是强符号。我查了一些资料,可能和链接顺序、编译器的weak符号处理有关。
删掉foc.c里的weak函数定义,只保留一行声明:
void set_pwm_duty(float d_u, float d_v, float d_w);
这样链接器只能去`main.c`里找实现,没有选择余地。重新编译烧录,程序不再卡死。
小结
weak符号的机制虽然灵活,但调试时容易让人抓狂——链接器选错版本时不会报错,运行时才暴露问题。如果是我自己写代码,我会在`foc.c`里只放声明不放定义,让链接器在编译期就报未定义符号,第一时间暴露问题。
总结
这两个问题花了我不少时间,记录几点心得
1. CubeMX里红色的配置项必须处理。ADC时钟超频这种问题,表面看和串口无关,实际上会影响整个系统
2. 调试时按信号链逐层隔离。从主循环→中断→算法函数→底层驱动,一层层加printf定位,比瞎猜有效得多。
3. weak符号有它的设计意图,但调试时要小心。链接器的行为依赖于工具链,跨编译器可能不一致。更安全的做法是只声明不定义。
后续会继续记录PID调参和VOFA+在线整定的过程。