使用SPI来与as5047P通信(获取角度,得到无刷电机当前信息)

之前提到了SPI通信,那今天就来实战一下,使用SPI来与as5047p通信获取角度,其中获取角度是控制无刷电机的基础。

SPI初始化和一些设置

首先是在cubemx中初始化SPI开启全双工主模式(Full-Duplex Master),将硬件NSS关闭,我们可以用软件模拟,选择一个GPIO输出拉低电平即可。

因为我们是要面向as5047P使用SPI通信,所以我们可以面向AS5047设计一个bsp这样也可以方便我们以后自己移植。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct MyAS5047p{
SPI_HandleTypeDef *hspi;//一个指向SPI_HandleTypeDef的指针
GPIO_TypeDef* CS_Port;//一个指向GPIO_TypeDef的指针
uint16_t CS_Pin;
uint8_t angle_refresh_cnt;
uint8_t angle_refresh_limit;
float angle_refresh_cycle;
float speed_rpm_param;
float speed_param;
float angle;
float last_angle;
float speed;
float speed_rpm;
float postion;

float speed_buf[10];
float speed_sum;
}MyAS5047p;

一个结构体类型定义,其中有两个指向结构体定义的指针,可以方便我们选择我们要使用的SPI和GPIO。还包含了一些我们控制无刷电机的必要参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 设置AS5047P SPI通讯模式*/
void _AS5047P_bsp_set_spi_mode( MyAS5047p *as5047p ){
if (HAL_SPI_DeInit( as5047p->hspi ) != HAL_OK)
{
// usb_printf("111");
while( 1 );
}
as5047p->hspi->Instance = as5047p->hspi->Instance;
as5047p->hspi->Init.Mode = SPI_MODE_MASTER;//SPI主模式
as5047p->hspi->Init.Direction = SPI_DIRECTION_2LINES;//全线全双工
as5047p->hspi->Init.DataSize = SPI_DATASIZE_16BIT;//数据帧大小
as5047p->hspi->Init.CLKPolarity = SPI_POLARITY_LOW;//空闲时钟线为低电平
as5047p->hspi->Init.CLKPhase = SPI_PHASE_2EDGE;//数据在时钟的第二个边沿采样
as5047p->hspi->Init.NSS = SPI_NSS_SOFT;//片选软件控制而不是硬件控制
as5047p->hspi->Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;//波特率预分频
as5047p->hspi->Init.FirstBit = SPI_FIRSTBIT_MSB;//高位开始
as5047p->hspi->Init.TIMode = SPI_TIMODE_DISABLE;
as5047p->hspi->Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;//CRC校验
as5047p->hspi->Init.CRCPolynomial = 10;
if (HAL_SPI_Init( as5047p->hspi ) != HAL_OK)
{
//usb_printf("222");
while( 1 );
}
}

这里是一些SPI的初始化配置,一般来说如果我们使用Cubemx会直接给我们封装好,但是这里我们要写一个bsp所以另外拿了出来。

主要函数编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
void _AS5047P_bsp_cs_H( MyAS5047p *as5047p ){
HAL_GPIO_WritePin(as5047p->CS_Port, as5047p->CS_Pin, GPIO_PIN_SET);
}
//这两个函数就是我们所说的软件拉低拉高电平进行片选
void _AS5047P_bsp_cs_L( MyAS5047p *as5047p ){
HAL_GPIO_WritePin(as5047p->CS_Port, as5047p->CS_Pin, GPIO_PIN_RESET);
}
//初始化一些传感器相关的数据结构
void AS5047P_bsp_init( MyAS5047p *as5047p ){
uint8_t i;
_AS5047P_bsp_cs_H( as5047p );//片选拉高禁用

as5047p->angle = 0.0f;
as5047p->last_angle = 0.0f;
as5047p->speed = 0.0f;
as5047p->postion = 0.0f;//位置
as5047p->angle_refresh_cnt = 0;//角度刷新计数器
as5047p->angle_refresh_cycle = 0.0001f; //角度刷新周期
as5047p->angle_refresh_limit = 5;//角度刷新限制值
as5047p->speed_param = ( 1.0f / ( as5047p->angle_refresh_cycle * as5047p->angle_refresh_limit )) * 0.017453292f; //弧度每秒 将角度转换为弧度
as5047p->speed_rpm_param = 9.5238095f;//将速度转换RPM
as5047p->speed_sum = 0.0f;
for( i = 0 ; i < 10 ; i++ )
as5047p->speed_buf[i] = 0.0f;//初始化这个数组
}
//发送
void _AS5047P_bsp_send( MyAS5047p *as5047p , uint16_t addr , uint16_t data ){
volatile uint8_t delay = 50;
uint8_t addrs[2];
uint8_t datas[2];
addrs[0] = addr >> 8;
addrs[1] = addr & 0x00ff;
datas[0] = data >> 8;
datas[1] = data & 0x00ff;
_AS5047P_bsp_cs_L( as5047p );//拉低启动
HAL_SPI_Transmit( as5047p->hspi , addrs , 1 , 0xffff );
_AS5047P_bsp_cs_H( as5047p );
while( delay != 0 )
delay--;
_AS5047P_bsp_cs_L( as5047p );//拉低启动
HAL_SPI_Transmit( as5047p->hspi , datas , 1 , 0xffff );
_AS5047P_bsp_cs_H( as5047p );//拉高停止
}
//接收
uint16_t _AS5047P_bsp_recv( MyAS5047p *as5047p , uint16_t addr ){
volatile uint8_t delay = 10;
uint16_t data;
_AS5047P_bsp_cs_L( as5047p );//拉低启动
HAL_SPI_TransmitReceive( as5047p->hspi , (uint8_t *)&addr , (uint8_t *)&data , 1 , 100 );
_AS5047P_bsp_cs_H( as5047p );//拉高停止
// addr = 0xC000;
while( delay--)
_AS5047P_bsp_cs_L( as5047p );//拉低启动
HAL_SPI_TransmitReceive( as5047p->hspi , (uint8_t *)&addr , (uint8_t *)&data , 1 , 100 );
_AS5047P_bsp_cs_H( as5047p );//拉高停止
return data;
}
//读取角度,包含了错误检测,奇偶校验,角度转换,速度计算,位置更新,返回角度值
float AS5047P_bsp_read_angle( MyAS5047p *as5047p , uint8_t daec_en ){
uint16_t addr;//设备地址
uint16_t data;//原始数据
uint8_t i,num = 0;//用于奇偶校验
addr = 0xffff;//daec_en ? 0xffff : 0x7ffe;
AS5047_RE_READ:
while( 1 ){
data = _AS5047P_bsp_recv( as5047p , addr );
if( (data & 0x4000) == 0 )
//错误位判断 检查数据的第 14 位(错误位),如果为 0 表示数据有效,否则重新读取。
break;
}
for(i=0;i<15;i++){
if((data >> i) & 1)
num += 1;
}
//奇偶校准
if((((num & 0x01) == 1) && ((data >> 15) == 0)) || ( (num & 0x01) == 0) && ((data >> 15) == 1)){
goto AS5047_RE_READ;//如果奇偶校验不对的话,就跳回去重新读取
}

data = (data & 0x3fff);//保留低 14 位数据(角度值)
as5047p->angle = (float)(data*360) / 16384.0f; //转换为电机角度 固定的

as5047p->angle_refresh_cnt += 1;//角度刷新计数器,用于控制速度计算的频率
//这么多次读取后计算一次角度
if( as5047p->angle_refresh_cnt == as5047p->angle_refresh_limit ){
float angle_error = ( as5047p->angle - as5047p->last_angle );
//计算角度的差值,防止角度突变
if( angle_error > 240.0f )
angle_error = angle_error - 360;
else if( angle_error < (-240.0f) )
angle_error = angle_error + 360;
//speed_param = (1.0f / (angle_refresh_cycle * angle_refresh_limit)) * 0.017453292f;每秒弧度的变化量
as5047p->speed = angle_error * as5047p->speed_param;//角度变化转换为速度
as5047p->speed_sum -= as5047p->speed_buf[0];
for( i = 0 ; i < 9 ; i++ ) {
as5047p->speed_buf[i] = as5047p->speed_buf[ i + 1];
}//移动位置 存储最近十个速度值的缓冲区
as5047p->speed_buf[9] = as5047p->speed;//添加最新的速度值
as5047p->speed_sum += as5047p->speed_buf[9];
as5047p->speed = as5047p->speed_sum / 10.0f;//计算平均速度,将Speed更新为缓冲区的平均值用于平滑速度数据
as5047p->speed_rpm = as5047p->speed * as5047p->speed_rpm_param;.//速度转化为转速

as5047p->postion += angle_error;//根据角度变化更新位置值
as5047p->last_angle = as5047p->angle;//存
as5047p->angle_refresh_cnt = 0;
}
return as5047p->angle;
}
void _AS5047P_bsp_send( AuroAS5047p *as5047p , uint16_t addr , uint16_t data ){
volatile uint8_t delay = 50;
uint8_t addrs[2];
uint8_t datas[2];
addrs[0] = addr >> 8;
addrs[1] = addr & 0x00ff;
datas[0] = data >> 8;
datas[1] = data & 0x00ff;//将数据拆分为高八位和低八位
_AS5047P_bsp_cs_L( as5047p );
HAL_SPI_Transmit( as5047p->hspi , addrs , 1 , 0xffff );
_AS5047P_bsp_cs_H( as5047p );
while( delay != 0 )
delay--;
_AS5047P_bsp_cs_L( as5047p );
HAL_SPI_Transmit( as5047p->hspi , datas , 1 , 0xffff );
_AS5047P_bsp_cs_H( as5047p );
}

uint16_t _AS5047P_bsp_recv( AuroAS5047p *as5047p , uint16_t addr ){
volatile uint8_t delay = 10;
uint16_t data;
_AS5047P_bsp_cs_L( as5047p );
HAL_SPI_TransmitReceive( as5047p->hspi , (uint8_t *)&addr , (uint8_t *)&data , 1 , 100 );
_AS5047P_bsp_cs_H( as5047p );
// addr = 0xC000;
while( delay--)
_AS5047P_bsp_cs_L( as5047p );
HAL_SPI_TransmitReceive( as5047p->hspi , (uint8_t *)&addr , (uint8_t *)&data , 1 , 100 );
_AS5047P_bsp_cs_H( as5047p );
return data;
}

使用和验证

1
2
3
4
5
MyAS5047p as5047p; 
as5047p.hspi = &hspi3;
as5047p.CS_Pin = SPI3_CS_Pin;
as5047p.CS_Port = SPI3_CS_GPIO_Port;
AS5047P_bsp_init( &as5047p);

再主函数中进行初始化相应的结构体,再进行初始化,获取as5047p读取的角度再用USB CDC发送出来。

PS:USBCDC不能ac6进行编译,得用ac5。

附上一个可以把float转字符串的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void myftoa(float num, char *str, int precision) {
// 处理负数
if (num < 0) {
str[0] = '-';
num = -num;
str++;
}

int integer_part = (int)num; // 提取整数部分
float fractional_part = num - integer_part; // 提取小数部分

// 处理整数部分
sprintf(str, "%d", integer_part); // 将整数部分转换为字符串
int len = strlen(str);

// 如果精度大于 0,处理小数部分
if (precision > 0) {
str[len] = '.'; // 添加小数点
len++;

// 将小数部分转换为整数
fractional_part = fabs(fractional_part); // 确保小数部分为正数
for (int i = 0; i < precision; i++) {
fractional_part *= 10; // 将小数部分放大
int digit = (int)fractional_part; // 提取当前位
str[len + i] = '0' + digit; // 转换为字符
fractional_part -= digit; // 移除已处理的位
}
str[len + precision] = '\0'; // 添加字符串结束符
} else {
str[len] = '\0'; // 如果没有小数部分,直接结束字符串
}
}