一.ADC(相电压、电流采集,总线电压,旋钮等)
1.普通单次转换ADC
已跑通,很简单,掌握几个核心函数就行
void knobs_task(void) {
osDelay(500);
while(1) {
HAL_ADC_Start(&hadc2);
HAL_ADC_PollForConversion(&hadc2, 1000);
adc2_value= HAL_ADC_GetValue(&hadc2); // 读取有效值
usb_printf("%lu\r\n", adc2_value);
osDelay(100);
}
}
void knobs_init(void) {
HAL_ADCEx_Calibration_Start(&hadc2, ADC_SINGLE_ENDED);
knobsTaskHandle=osThreadNew(knobs_task, NULL, &knobsTask_attributes);
}

2.普通连续转换ADC
出现问题并未跑通,十分疑惑,把OE位置于0就可以进行正常读取,或者打开Low Power Auto Wait也可以解决。已有人遇到过相同的问题,但并未找到问题的根本
32ADC单通道连续模式只能采样一次问题,adc值不更新( CUBMX+HAL)_循环读取内部adc的值不变化-CSDN博客
void knobs_task(void) {
HAL_ADC_Start(&hadc2);
HAL_ADC_PollForConversion(&hadc2, 1000);//1000和HAL_MAX_DELAY都试过没有变换
osDelay(500);
while(1) {
adc2_value= HAL_ADC_GetValue(&hadc2); // 读取有效值
usb_printf("%lu\r\n", adc2_value);
osDelay(100);
}
}
void knobs_init(void) {
HAL_ADCEx_Calibration_Start(&hadc2, ADC_SINGLE_ENDED);
knobsTaskHandle=osThreadNew(knobs_task, NULL, &knobsTask_attributes);
}3.DMA连续多通道ADC转换!!
要点:
1.SCAN模式打开,各RANK的通道设置正确,连续转换,打开DMA,DMA中设置循环,通道随便选?
2.DATA大小设置的要和代码接收的要一致(word->uint32_t,byte->uint8_t)
3.DMA Continuous Requests要打开!KK视频中并未提到这一点,若不打开会一直为零!!
或者获取一个值后不进行更新。
4.Clock Prescaler要设置改一点,默认参数(4分频)可能对于ADC处于超频状态,若不改此参数上述操作正确,会出现数据打印慢、不定时打印的情况,25.8.22现在ADC2的四分频还出现了freertos卡死严重拖慢的问题,还出现了注释start出现freertos卡死,取消注释又正常的情况,十分玄学。目前猜测时钟太快在任务不重的情况下问题不大,任务或者中断复杂起来就会出现问题。后续搞清楚这几个分频模式有什么区别!!!
现在降低分频发现freertos没问题了,但是HAL_ADC_Start_DMA完之后反而没有数值
uint32_t dma_adc2_buffer[4];
HAL_ADC_Start_DMA(&hadc2, (uint32_t*)dma_adc2_buffer, sizeof(dma_adc2_buffer)/sizeof(uint32_t));
while(1){
}
二、SPI通讯(编码器读取电机角度)
1.径向磁铁安装注意还是要大一点的比较好,本次使用的偏小为直径3mm


2.CPOL和CPHA的选择,根据芯片手册来,看他的SCLK初始是高还是低(看时序图可知MT6701为初始低电平),然后看他说建议上升沿还是下降沿采集,CUBEMX中描述有些许差别,为第一个跳变沿或第二个跳变沿开始隔一个开始采集,大同小异,由此开始配置CUBEMX



为什么这里选择8bits?磁编码器发送的数据不是24bit(14位数据+4位状态+6位CRC)吗?因为我们选择uint8_t,后刚好24位可以被uint8_t txData[3];接收完,然后我们将uint8_t txData[0]和uint8_t txData[1]的前六位进行运算即可。还有另一种方式就是选16bits然后一次接16bit(按8bit分割)在右移两位去掉两位状态即为纯角度。
HAL_StatusTypeDef spiStatus = HAL_SPI_TransmitReceive(&hspi1, (uint8_t *)&txData, (uint8_t *)&rawData, 1, HAL_MAX_DELAY);//第二种方式读取,不知道为什么HAL_SPI_TransmitReceive就是用不了,需要改成纯Receive,而且MT6701就没有MISO线,发送什么?
而且这里的(uint8_t *)我也不是很理解,16位数据直接右移两位不就行了吗?这样分割是为了什么?下面的uint16_t Size参数是什么意思?为什么填1也可以?接受的数据不是3个8bit吗?
/**
* @brief Receive an amount of data in blocking mode.
* @param hspi pointer to a SPI_HandleTypeDef structure that contains
* the configuration information for SPI module.
* @param pData pointer to data buffer
* @param Size amount of data to be received
* @param Timeout Timeout duration
* @retval HAL status
*/
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)附上修改后的MT6701磁编码器代码,十分稳定
8.23现在发现此编码器在cubeMX中速率不能设置太高,尝试提高到20m时发现电机编码器到MCU的线路十分敏感,稍微挪个位置或者用手捏电机就会发出炒豆声,5m又太慢了也炒豆。所以现在恢复成了10m
//
// Created by 29569 on 25-4-27.
//
#include "mt6701_spi.h"
#include "arm_math.h"
#include "cmsis_os2.h"
#include "spi.h"
#include "stm32g4xx_hal_spi.h"
#include "FreeRTOS.h"
#include "task.h"
#include "usbd_cdc_if.h"
osThreadId_t mt6701_spiTaskHandle;
const osThreadAttr_t mt6701_spiTask_attributes = {
.name = "mt6701_spiTask",
.priority = (osPriority_t) osPriorityNormal,
.stack_size = 512*4
};
#define MT6701_CS_Enable() HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET)
#define MT6701_CS_Disable() HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET)
int rotationCount = 0; // 旋转过的圈数
int rotationCount_Last; // 上一次循环时转过的圈数
uint16_t MT6701_GetRawData(void)
{
uint16_t rawData;
uint16_t timeOut = 200;
while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY)
{
if (timeOut-- == 0)
{
return 0; // 在超时时直接返回,避免继续执行后续代码
}
}
MT6701_CS_Enable();
HAL_StatusTypeDef spiStatus = HAL_SPI_Receive(&hspi1, (uint8_t *)&rawData, 2, 200);
if (spiStatus != HAL_OK)
{
MT6701_CS_Disable();
rawData = 0;
return 0; // 在SPI传输错误时直接返回,避免继续执行后续代码
}
MT6701_CS_Disable();
return rawData >> 2; // 取高14位的角度数据
}
float MT6701_GetRawAngle(void)
{
uint16_t rawData = MT6701_GetRawData();
return (float)rawData / 16384.0f * 2*PI;
}
// 获得转过的总角度,有圈数累加
float MT6701_GetFullAngle(void)
{
static float angle_Last = 0.0f; // 上次的轴角度,范围0~6.28
float angle = MT6701_GetRawAngle(); // 当前角度,范围0~6.28
float deltaAngle = angle - angle_Last;
// 计算旋转的总圈数
// 通过判断角度变化是否大于80%的一圈(0.8f*6.28318530718f)来判断是否发生了溢出,如果发生了,则将full_rotations增加1(如果d_angle小于0)或减少1(如果d_angle大于0)。
if (fabsf(deltaAngle) > (0.8f * 6.28318530718f))
{
rotationCount += (deltaAngle > 0) ? -1 : 1; // 圈数计算
rotationCount_Last = rotationCount;
}
angle_Last = angle;
return rotationCount * 6.28318530718f + angle_Last; // 转过的圈数 * 2pi + 未转满一圈的角度值
}
// 计算转速
float MT6701_GetVelocity(void)
{
static float full_Angle_Last = 0.0f; // 记录上次转过的总角度
float full_Angle = MT6701_GetFullAngle();
float delta_Angle = (rotationCount - rotationCount_Last) * 2*PI + (full_Angle - full_Angle_Last);
float vel = delta_Angle * 1000.0f; // Ts = 1ms
// 更新变量值
full_Angle_Last = full_Angle;
return vel;
}
void mt6701_spi_task(void) {
osDelay(500);
while (1) {
usb_printf("SPI:%f\r\n", (float)MT6701_GetVelocity());
//usb_printf("TEST剩余栈:%d,%d\r\n", (int)uxTaskGetStackHighWaterMark(NULL), (int)xPortGetFreeHeapSize());
osDelay(5);
}
}
void mt6701_spi_init(void) {
mt6701_spiTaskHandle=osThreadNew(mt6701_spi_task, NULL, &mt6701_spiTask_attributes);
}
STM32 HAL库spi读取mt6701角度值_mt6701 spi-CSDN博客
STM32CubeMX学习笔记(1)--SPI接口使用(读取MT6701位置传感器)1.MT6701简介 1.1 IIC - 掘金(未实现功能)
三、FreeRTOS(底层系统框架)
freertos基本概念、任务创建和删除就不多说了,注意的是Linux和windows都为通用操作系统(general-purpose OS)。
freertos的堆是在全局区域里创建的,并非占用系统堆。
freertos堆大小,分配方法由下图参数决定
堆和栈的分配方式:
FreeRTOS提供了多种堆管理方案,可以在FreeRTOSConfig.h文件中选择使用哪种方案。
heap_1:只支持静态分配,即在程序开始时就分配好所有任务的TCB和栈空间,不支持动态创建和删除任务。
heap_2:支持动态分配,即在程序运行时可以创建和删除任务,但不支持内存回收,即删除任务后不会释放其占用的内存空间。
heap_3:支持动态分配和内存回收,使用标准C库的malloc和free函数来管理堆空间,但可能存在内存碎片问题,即堆空间被分割成很多小块,导致无法分配足够大的连续空间。
heap_4:支持动态分配和内存回收,并且可以合并相邻的空闲块,减少内存碎片问题,但需要更多的代码空间和执行时间。
heap_5:在heap_4的基础上增加了多个堆区域的支持,可以将不同大小或者不同属性的内存区域作为堆来使用,提高了内存利用率。
注意若选择heap_5则创建线程需要在osKernelInitialize();之后,否则会卡在申请空间。
若想要查看堆栈剩余情况,可使用以下函数打印出情况
usb_printf("TEST最小剩余栈%d\r\n",(int)uxTaskGetStackHighWaterMark(NULL));//查看栈最小时候的值
usb_printf("TEST剩余栈%d\r\n",(int)xPortGetFreeHeapSize());//查看栈的值
configUSE_NEWLIB_REENTRANT参数和printf、sprintf之类有关,关掉的话就没有前述函数的库(NEWLIB)无法进行打印输出
堆栈溢出检测(CHECK_FOR_STACK_OVERFLOW)需要写回调处理函数,懒得写固暂时不开
对于创建任务的两个基本函数xTaskCreate()和osThreadNew()我们这里选用xTaskCreate(),因为osThreadNew()函数创建起来参数需要的更加多一些,并且xTaskCreate()更加通用一些
// osThreadNew最小创建
osThreadId_t myThreadId = osThreadNew(myTaskFunc, NULL, &(const osThreadAttr_t){
.name = "MyOneLineTask",
.priority = osPriorityNormal,
.stack_size = 512, // 示例栈大小,请根据实际需要调整
.cb_mem = NULL, // 动态分配TCB
.cb_size = 0,
.stack_mem = NULL // 动态分配栈空间
});
//xTaskCreate最小创建
xTaskCreate(myTaskFunc, "MyOneLineTask", 128, NULL, osPriorityNormal, NULL);//最后参数为myThreadId若不需要此功能可直接NULL
因为freertos任务中osdelay做少都要1ms,导致电机控制频率受限。纯裸机或者放在中断内的话又会导致系统可读性差。
其实我们可以用freertos的通知功能让freertos也拥有接近中断的性能,就是可以用定时器内通知freertos的任务,任务会被立刻换新执行,效果类似于把程序直接放在了中断内,这样我们就可以既有中断般的控制性能,又可以拥有freertos的多线程能力,易读性也会大大提升。
我们只需要在中断中加入下面的代码,把任务句柄改成你的就可以了
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (FOC_Basic_taskHandle != NULL) {
// 这个函数非常快,它只是在任务的 TCB 中设置一个标志位
vTaskNotifyGiveFromISR(FOC_Basic_taskHandle, &xHigherPriorityTaskWoken);
}
//如果更高优先级的任务被唤醒,请求立即进行上下文切换
//这是保证低延迟的核心!没有这一行,延迟会非常高!
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
然后在主程序中的while1中等待即可(不需要再osdelay了)
//等待通知
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);【FreeRTOS】FreeRTOS学习笔记 ---- 堆和栈,第1个FreeRTOS程序,创建任务函数及任务管理_freertos的堆栈-CSDN博客
STM32CubeMX FreeRTOS堆栈分配、调试技巧-腾讯云开发者社区-腾讯云
FreeRTOS记录(一、熟悉开发环境以及CubeMX下FreeRTOS配置) - 知乎
(2 条消息) FreeRTOS中关于任务创建osThreadNew()函数详解 - 知乎
四、PWM(最终三相电压输出)
1.基本配置
Center Aligned Mode 1 2 3的区别,简单来说区别就是中断产生的时机不同,mode3为高点低点都产生中断
我们FOC需要再最高点采样(因为最高点电平稳定),所以配置为模式二
上面的说法不对,Center Aligned Mode只会影响TIM1 Update Interrupt中断的条件,和后面我们用定时器触发ADC采样的那个TRGO条件不一样,我们可以理解为不管你怎么设置,高低点都是是会产生中断的,而中心对齐的这三个模式只是在最后进行了一个条件筛选。TRGO触发条件在中心对齐模式筛选之前,所以Center Aligned Mode三种中断不影响TRGO触发条件

PWM模式1:无论是向上计数还是向下计数,只要CNT ⩽CCRx,PWM输出低电平(CCR值越高占空比越高)。
PWM模式2:无论是向上计数还是向下计数。只要CNT ⩽CCRx,PWM输出高电平(CCR值越高占空比越低)。
下面这个才是正确的,有效电平是什么取决于PWM配置中CH polarity参数的选择

我们这里选择模式1,CH polarity为高,所以效果为占空比越高,高电平占比越大。
还有就是老生常态的PWM频率的计算了,这里直接贴出计算公式
普通模式PWM频率计算:f_PWM = (定时器时钟频率 / (PSC + 1)) / (ARR + 1)
反推ARR公式:ARR = ( (定时器时钟频率 / (PSC + 1)) / f_PWM ) - 1
中心对齐模式PWM频率计算:f_PWM = (定时器时钟频率 / (PSC + 1)) / (2 * ARR)
反推ARR公式ARR = (定时器时钟频率 / (PSC + 1)) / (2 * f_PWM)
注意这里的定时器时钟频率是APB1或者APB2的时钟频率,并不是你设置的主频(HCLK)
死区时间暂不计算,FD6288Q内部自带200ns的默认死区时间
最重要的是下面的
2.定时器触发ADC注入组采样(重要!)
首先我们为什么做这个操作呢?上面的DMA连续采样不是也挺好的吗?是挺好的,但是这样就是随机乱采样,这样的采样会给电流获取带来很多噪声,最好是在一个系统稳定的时候进行采样就好了,这个系统稳定的状态就是上管全打开的时候。对应PWM的CNT最大的时候。此处勘误,如果为上管全打开时采样(如图1)就会因为采样电阻没有电流导致采样失败,应该在下管全打开的时候采样。因为我们大部分是低侧采样,所以下管全打开的情况下三个电阻(双电阻采样同理,三电阻优于双电阻的原因就是可以根据扇区去选择下管打开时间长或者说稳定的两项进行计算)由于电机电感续流作用三个采样电阻都可以有数值,并且三段接通(如下图2),这也是为什么我们可以用KCL去计算的理论依据。此处不明白请跳转至本节链接4


下图采样为错误示范,跳变点应该在上三相低电平中点(对应Pulse=ARR)

下面我们进行实际操作,首先我们先配置触发源。配置定时器1 CH4作为触发源

设置CH4的跳变判断条件为CNT=0,这样当计时器数到0的时候(在CNT三角在谷底的情况)CH4会进行一次跳变,也会通过TRGO传递给ADC。

对比大部分其他人的,通过PWM Generation No Output 再通过调整Pulse(Pulse略小于ARR10左右)的方案,本方案更具备更直观、科学的优点,因为PWM Generation方案Pulse不能调整到ARR或者0(太极限检测不到),会导致需要使用者去猜和多次实验使其居中。

接下来我们配置ADC注入组

最后在修改代码
中断中尽量不要有耗时的事件比如下面代码的usb_printf,亲测这样会导致中断被跳过一次,间隔触发。
在实验中也发现串口发送函数printf在上位机连接上的情况下对系统的资源占用越大(连接上后电机发出明显的炒豆声),上位机未连接的情况下会占用更少的系统资源但也会导致中断异常。
//在初始化的时候使能中断
__HAL_ADC_ENABLE_IT(&hadc1, ADC_IT_JEOC);//可不要
HAL_ADCEx_InjectedStart_IT(&hadc1);//必须加上
//也别忘了使能TIM1 CH4
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4);//必要
// 全局变量,用于存储ADC值,可采用数组方式,大同小异
volatile uint32_t adc_val_ia = 0;
volatile uint32_t adc_val_ib = 0;
volatile uint32_t adc_val_ic = 0;
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef* hadc) {
// 确保是正确的ADC触发的
if (hadc->Instance == ADC1) {
// HAL_GPIO_WritePin(GPIOC, GPIO_PIN_3, GPIO_PIN_SET);
// HAL_GPIO_WritePin(GPIOC, GPIO_PIN_3, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
// 1. 快速读取ADC数据到全局变量
// 注意:Rank 1对应JDR1,Rank 2对应JDR2
adc_val_ia = HAL_ADCEx_InjectedGetValue(hadc, ADC_INJECTED_RANK_1);
adc_val_ib = HAL_ADCEx_InjectedGetValue(hadc, ADC_INJECTED_RANK_2);
adc_val_ic = HAL_ADCEx_InjectedGetValue(hadc, ADC_INJECTED_RANK_3);
// usb_printf("ADC:%d,%d,%d\r\n",adc_val_ia,adc_val_ib,adc_val_ic);//引起中断跳过执行的原因
svpwm_open_loop_control();
//
// // 2. 发送通知给FOC任务
// BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 初始化标志
// //vTaskNotifyGiveFromISR(foc_task_handle, &xHigherPriorityTaskWoken);
//
// // 3. 如果有更高优先级的任务被唤醒,立即进行任务切换
// portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
我们在ADC中断中翻转IO口进行测试。发现符合预期。稍微滞后是因为ADC采样和中断向量的传递也需要时间

附上两张在pulse在0和ARR时的Ialpha,Ibeta采样结果图

修改采样时机后的Ialpha,Ibeta波形

STM32三种对齐计数模式及其中断回调函数——用CubeMX工具_stm32 timer三种中心对齐计数模式区别-CSDN博客
STM32Cube的PWM控制基础篇(三)定时器的PWM设置详解_ch idle state-CSDN博客(PWM里各个参数的含义此文章中都有解释)
STM32CubeMX学习笔记(6)--定时器触发ADC采样1.PWM mode1和PWM mode2的区别 总结: P - 掘金(又是他)
foc配置篇——ADC注入组使用定时器触发采样的配置-CSDN博客
foc控制中三电阻电流采样思路分析(解释了为什么要在下管全开时进行采样)
其他疑惑
1.这个栈大小怎么知道多少合适?少了会导致各种奇奇怪怪的问题,在连续转换ADC实验时发现大了的话就无法连上串口,已经控制变量进行过实验


2.为什么ADC的值要用ADC:%lu,%lu,%lu,%lu才能打印?而%f不行?
3.为什么这边最小栈为128,但是我LED闪烁的栈分配了8还是能运行?


4.为什么栈大小同为12000,堆和栈的分配方式设置为heap_5就USB打印不出来了,USB灯也不闪了,Heap_4就
评论区