ICM20948 datasheet
AK09916 datasheet
쿼드콥터와 헥사콥터를 만들 때 가장 필요한 부품은 기체의 각도를 알기위해 사용하는 자이로센서이다. 이를 위해 MPU6050 IC를 사용하여 각도를 측정하고 있다.
하지만 MPU6050은 I2C 통신을 사용하는데 각속도와 가속도 값을 읽는데 Fast Mode 400kHz 기준 400us가 조금 넘는 통신시간이 걸린다.
DMA를 사용한다 해도 결국 새로운 값을 얻는데 400us의 시간을 기다려야 한다.
현재 쿼드콥터, 헥사콥터에 사용하는 ESC의 경우 일반적인 PWM신호인 1ms~2ms 신호를 받지만 만약 Oneshot125, Oneshot42 등등 더 빠른 속도를 지원하는 ESC를 사용하여 500Hz보다 빠른 속도로 모터를 제어하기 위해서는 I2C 통신시간을 줄여 각도를 더 빠르게 얻어낼 필요가 있다.
따라서 I2C보다 통신속도가 더 빠른 방식인 SPI 통신을 사용하는 자이로 센서 중 비슷한 이름을 가진 MPU6000에 대해 찾아봤지만 가격이 비싸고 간단하게 사용할 수 있는 모듈 형태가 없었다.
그러다 pixhawk에서 ICM20948 칩을 사용한다는 글이 있었고 ICM20948 모듈을 판매하고 있어 구입해봤다.
하지만 구입한 모듈에는 I2C 통신 핀만 있었고 SPI 통신에 관한 핀이 없었다.
SPI 통신 핀이 모듈에 없어도 모듈을 제어하는 레지스터는 동일하고 통신부분만 다를 것이므로 I2C 통신으로 제어하는 코드를 먼저 작성하고 나중에 SPI 통신 핀이 존재하는 모듈이 도착하면 통신부분만 바꿔서 사용하기로 했다.
따라서 이번 글에는 SPI 통신에 관한 글보다 I2C 통신에 관해 작성할 것이다.
SPI핀이 나와있는 모듈은 추가로 구입했으며 도착하면 그때 코드를 작성하면 추가로 내용을 쓸 것 같다.
ICM20948은 기본적인 6축 자이로에 3축 지자기 센서 AK09916이 내부에 들어가 있다.
ICM20948 데이터시트 외에 AK09916 데이터시트도 잘 봐야 한다.
I2C 코드 생성 - (0)
I2C는 I2C1, I2C2, I2C3 아무거나 사용하면 되며 Speed Mode의 경우 원하는 Mode를 사용하면 된다.
Interrupt와 DMA는 사용하지 않는다.
TIM1 코드 생성 - (1)
2ms를 만들기 위한 TIM1 설정이다.
이미 많이 설명했기 때문에 따로 설명은 하지 않는다.
코드 - main.c (2)
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
|
/* USER CODE BEGIN 0 */
int _write(int file,uint8_t* p, int len){
for(int i=0;i<len;i++){
LL_USART_TransmitData8(USART1,p[i]);
while(!LL_USART_IsActiveFlag_TXE(USART1));
}
return len;
}
uint8_t condition_2ms=0;
/* USER CODE END 0 */
/* USER CODE BEGIN 2 */
uint16_t counter=0;
int16_t magdata_int[3];
float magdata[3];
LL_mDelay(1000);
printf("start\r\n");
init_ICM20948(&icm20948,I2C3);
printf("%.2X\r\n",ICM_who_am_i(&icm20948));
init_AK09916(&icm20948);
printf("%.2X\r\n",AK_Company_ID(&icm20948));
printf("%.2X\r\n",AK_Device_ID(&icm20948));
ICM_Gyrocali(&icm20948);
ICM_Angcali(&icm20948);
LL_TIM_SetAutoReload(TIM1,2000);
LL_TIM_EnableIT_UPDATE(TIM1);
LL_TIM_EnableCounter(TIM1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if(condition_2ms){
ICM_ComplementaryFilter(&icm20948);
AK_ReadData(&icm20948,magdata,magdata_int);
counter++;
if(counter>100){
printf("%d %d %d\r\n",magdata_int[0],magdata_int[1],magdata_int[2]);
printf("%.2f\t%.2f\n\n\r",icm20948.pitch,icm20948.roll);
counter=0;
}
condition_2ms=0;
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
|
USER CODE 0 부분은 main함수 밖에 존재하는 코드이며, USER CODE 2의 부분은 main함수 내부에 존재한다.
18번째 줄부터 20번째 줄은 테스트하기 위한 변수들이다. magdata_int는 raw데이터를 저장하고 magdata는 실수 형태로 변환한 값을 저장하는데 지자기 센서를 사용하여 바라보는 각도를 구할 때 실수 형태의 값은 딱히 필요가 없다.
24번째 줄부터 30번째 줄은 기본적인 초기화를 하는 과정이다.
init_ICM20948은 칩 내부 레지스터를 초기화하고 init_AK09916은 내부에 내장되어있는 지자기 센서를 초기화한다.
지자기 센서를 제어하는데 몇 가지 방법이 있지만 이번 글에서는 직접 접근하는 방식을 사용할 것이다.
그 외 칩의 ID들을 출력하여 데이터 시트에 나온 값과 같은지 확인하고 MPU6050의 코드처럼 offset을 구하고 TIM1을 실행하는 코드들이 있다.
while문 내부는 상보필터를 사용하여 각도를 구하고 지자기 센서에서 값을 읽고 출력하는 코드이다.
icm20948 구조체 변수는 ICM20948.h에 정의되고 선언되어 있다.
stm32f4xx_it.c의 파일은 condition_2ms를 TIM1 UPDATE Interrupt마다 1로 SET 하는 코드만 확인하면 된다.
코드 - ICM20948.c (3)
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
|
//------------------------------------------------------------------------ ICM20948
void init_ICM20948(ICM20948* icm20948,I2C_TypeDef* I2C){
icm20948->i2c.I2C=I2C;
icm20948->gyro_address=ICM20948_ADDRESS<<1;
icm20948->magneto_address=AK09916_ADDRESS<<1;
LL_mDelay(1000);
bank_select(icm20948,0);
Gyro_Writebyte(icm20948,PWR_MGMT_1,0x80);
LL_mDelay(100);
Gyro_Writebyte(icm20948,PWR_MGMT_1,0x09);
bank_select(icm20948,2);
Gyro_Writebyte(icm20948,GYRO_SMPLRT_DIV,0x01);
Gyro_Writebyte(icm20948,GYRO_CONFIG_1,0x03);
Gyro_Writebyte(icm20948,ACCEL_SMPLRT_DIV_2,0x01);
Gyro_Writebyte(icm20948,ACCEL_CONFIG,0x03);
bank_select(icm20948,0);
Gyro_Writebyte(icm20948,INT_PIN_CFG,0x02); //bypass enable
}
void bank_select(ICM20948* icm20948,uint8_t bank){
if(BANK!=bank && bank<4){
Gyro_Writebyte(icm20948,REG_BANK_SEL,bank<<4);
BANK=bank;
}
}
void ICM_Readaccgyro(ICM20948* icm20948){
int16_t temp[3];
int16_t tempaccx,tempaccy,tempaccz;
uint8_t databuf[12];
uint8_t reg;
bank_select(icm20948,0);
reg=ACCEL_XOUT_H;
I2C_Transmit(&icm20948->i2c,icm20948->gyro_address,®,1);
I2C_Receive(&icm20948->i2c,icm20948->gyro_address,databuf,12);
tempaccx=(int16_t)(databuf[0]<<8 | databuf[1]);
tempaccy=(int16_t)(databuf[2]<<8 | databuf[3]);
tempaccz=(int16_t)(databuf[4]<<8 | databuf[5]);
temp[0]=(int16_t)(databuf[6]<<8 | databuf[7]);
temp[1]=(int16_t)(databuf[8]<<8 | databuf[9]);
temp[2]=(int16_t)(databuf[10]<<8 | databuf[11]);
icm20948->getaccx=tempaccx;
icm20948->getaccy=tempaccy;
icm20948->getaccz=tempaccz;
icm20948->temp[0]=temp[0];
icm20948->temp[1]=temp[1];
icm20948->temp[2]=temp[2];
icm20948->f_gyx=((float)(temp[0]-icm20948->gyro_offset[0]))/65.5;
icm20948->f_gyy=((float)(temp[1]-icm20948->gyro_offset[1]))/65.5;
icm20948->f_gyz=((float)(temp[2]-icm20948->gyro_offset[2]))/65.5;
/*printf("%d %d %d\r\n",icm20948->getmpuaccx,icm20948->getmpuaccy,icm20948->getmpuaccz);
printf("%.1f %.1f %.1f\r\n\n\r",icm20948->f_gyx,icm20948->f_gyy,icm20948->f_gyz);*/
}
//------------------------------------------------------------------------ AK20948
void init_AK09916(ICM20948* icm20948){
LL_mDelay(300);
Mag_Writebyte(icm20948,AK09916_CNTL2,0x08); //mode4
}
void AK_ReadData(ICM20948* icm20948,float* data,int16_t* data_int){
uint8_t receive[6];
int16_t temp[3];
uint8_t reg;
if(!(Mag_Readbyte(icm20948,AK09916_ST1) & 0x01)){
return;
}
reg=AK09916_XOUT_L;
I2C_Transmit(&icm20948->i2c,icm20948->gyro_address,®,1);
I2C_Receive(&icm20948->i2c,icm20948->magneto_address,receive,6);
temp[0]=(int16_t)(receive[1]<<8 | receive[0]);
temp[1]=(int16_t)(receive[3]<<8 | receive[2]);
temp[2]=(int16_t)(receive[5]<<8 | receive[4]);
//data[0]=((float)temp[0])/6.66;
//data[1]=((float)temp[1])/6.66;
//data[2]=((float)temp[2])/6.66;
data_int[0]=temp[0];
data_int[1]=temp[1];
data_int[2]=temp[2];
Mag_Readbyte(icm20948,AK09916_ST2);
}
|
중요하다고 생각하는 함수 5가지이다.
ICM20948 칩에서 중요한 것은 레지스터들이 BANK에 따라서 분리되어 있다는 것이다. 0부터 3의 4개의 BANK가 존재하는데 이를 바꿔줘야 원하는 레지스터에 접근할 수 있다. 이때 bank_select() 함수를 사용하여 변경한다.
자이로와 가속도 센서의 ODR을 정할 때 2번 BANK에 있는 GYRO_SMPLRT_DIV, GYRO_CONFIG_1, ACCEL_SMPLRT_DIV_1, ACCEL_SMPLRT_DIV_2, ACCEL_CONFIG 레지스터를 사용하여 정해줄 수 있는데
5개의 레지스터와 사진을 참고하여 각각 해당하는 값을 찾아 설정해주면 된다.
현재 코드는 각각 약 500Hz로 설정했다.
그리고 AK09916 칩에 직접 접근하기 위해 INT_PIN_CFG 레지스터에서 BYPASS_EN을 SET 하여 직접 접근이 가능하도록 설정했다.
ICM_Readaccgyro() 함수는 ACCEL_XOUT_H레지스터부터 12개의 레지스터를 읽어 각속도와 가속도를 읽고 구하는 함수이다.
AK09916에서 놓칠 수 있는 부분이 있어 AK_ReadData() 함수 코드를 넣었다.
데이터시트에서 놓칠 수 있는 부분이다. 바로 데이터를 읽고 ST2 레지스터를 읽어야 다음 데이터를 읽을 수 있다는 점이다. 이를 놓치면 제대로 된 데이터를 읽지 못할 수 있다.
결과 - (4)
실제 실행하면 나오는 결과이다.
0xEA는 ICM20948의 ID이고, 0x48과 0x09는 AK09916의 ID이다.
지자기 센서의 값은 raw값을 출력한다.
-
현재 제작한 쿼드콥터와 헥사콥터는 일반적인 최대 500Hz PWM 신호를 입력으로 받는다.
하지만 더 세밀하게 제어하기 위해 Oneshot125를 지원하는 ESC로 새로운 쿼드콥터를 만들 생각을 하고 있는데 I2C 외에 더 빠른 SPI를 사용하면 더 세밀하게 제어할 수 있지 않을까 하는 생각에 ICM20948 코드를 작성했다.
MPU6050을 대신하기 위해 사용할 생각이었지만 실제로 ODR은 엄청 빠른 편이 아닌 것을 데이터시트에서 찾을 수 있다.
최대 속도가 9kHz, 4.5kHz라고 하지만 실제 사용해보면 값이 좀 튀는 모습을 보였다.
속도를 해결하려면 단순히 칩을 변경하는 것 외에 다른 방법이 필요해 보인다.
아직 SPI코드는 작성하지 않았지만 통신하는 부분의 코드만 변경하면 바로 사용할 수 있다고 생각한다.
레지스터에 관한 내용은 데이터시트를 참조하면 더 많은 정보를 얻을 수 있을 것이다.
'임베디드 > STM32' 카테고리의 다른 글
[STM32] LL 드라이버 - MS5611 (0) | 2020.08.28 |
---|---|
[STM32] LL 드라이버 - SPI DMA, NRF24L01 (1) | 2020.08.20 |
[STM32] LL 드라이버 - M8N GPS (0) | 2020.08.04 |
[STM32] LL 드라이버 - nrf24l01 (0) | 2020.07.28 |
[STM32] LL 드라이버 - ADC, DAC, DMA (0) | 2020.07.27 |