Category (Click)
개발보드 덕질하기

[NUCLEO-F103RB] ICM20948 가속도, 자이로, 지자기 I2C+DMA+INT (HAL)

 차완기 - @9/10/2023, 6:18:00 PM

식어버린 국밥, MPU9250

최근 진행중인 프로젝트에서 IMU를 사용할 일이 있었습니다. 가속도, 자이로 센서만 필요하다면 국밥인 MPU6050으로 충분했지만, Yaw축 보정을 위해 지자기 센서가 추가로 필요한 상황이었습니다.
문제는 마찬가지로 국밥이었던 MPU-9250이 한참 전에 단종되어 이제는 정품칩을 구할 방법이 전혀 없다는 것이죠. (MPU6050도 NRFND라 대체해야하긴 합니다)
HMC5883같은 만만한 지자기센서 하나를 더 달까 하다가 이참에 대체품인 ICM-20948 드라이버를 하나 만들어 계속 우려먹기로 했습니다.

ICM-20948 VS MPU-9250

TDK에서도 이제 그만 9250은 놓아달라는것인지 마이그레이션 가이드라인까지 배포하고 있었습니다.
AN-000146-v2.0-TDK_Migration_MPU_9250toICM-20948.pdf
MPU-9250/55
ICM-20948
요약
가속도 범위
±2~±16 g
±2~±16 g
동일
자이로 범위
±250~±2000 dps
±250~±2000 dps
동일
지자기 범위
4800 µT
4900 µT
상위호환
DMP
6-axis fusion
9-axis fusion
상위호환
통신
I2C Slave: 400 KHz I2C Master: Auxiliary, 400 KHz SPI: 1 MHz
I2C Slave: 400 KHz I2C Master: Auxiliary, 400 KHz SPI: 7 MHz
호환
I2C 주소
7b’110100x
7b’110100x
동일
사양적인 부분은 위와 같습니다. 대부분의 사양은 동일하거나 소폭 상위호환이라 MPU9250을 대체하기에는 충분해 보입니다.
문제는 레지스터가 많이 바뀌었다는 것인데요,, 요건 데이터시트를 하나하나 읽어봐야했습니다.

데이터시트 훓어보기

DS-000189-ICM-20948-v1.5.pdf

다이어그램

ICM-20948는 조금 특이하게 하나의 센서로 구성된 것이 아닌 두 개의 센서로 구성되어있습니다. 가속도 3축, 자이로 3축, 온도 이렇게 3개의 센서가 센서 내장 레지스터에 연결되어있고, 지자기 센서인 AK09916 센서가 ICM-20948에 내장되어있습니다.
이때 AK09916 센서는 I2C Bypass MUX를 통해 크게 2가지 방법으로 사용할 수 있습니다.
1.
센서 내장 I2C Master을 활용해 ICM-20948이 직접 데이터를 읽음. (MCU측에서 하나의 주소만 인식됨)
2.
센서 자체 I2C 버스에 직결해 MCU측에서 직접 제어함. (MCU측에서 두개의 주소가 인식됨)
저는 DMA를 이용해 센서의 레지스터 데이터를 읽을건데요, 이때 주소가 둘로 나뉘어버리면 DMA 호출을 2번 해야하기 때문에 ICM-20948을 경유해 지자기 데이터를 읽는 1번 방법을 사용하였습니다.

REGISTER BANK

내장된 센서가 많고 기능도 다양하다 보니 하나의 레지스터 범위로는 해결이 안되었나봅니다. 0~3까지 총 4개의 Register Bank가 존재하며, 이를 전환해가면서 레지스터를 건들여야합니다.
모든 레지스터 뱅크의 고정된 주소(127, 0x7F)에는 레지스터 뱅크를 전환하는 레지스터가 존재합니다. 이를 활용해 레지스터 뱅크를 전환하는것이죠.

외장 센서 데이터 영역

앞서 언급한 내용처럼 ICM-20948은 Master모드를 지원해 자기 자신에 연결된 센서(auxiliary)의 데이터를 가져올 수 있습니다. 최대 5개의 주소를 지원하며, BANK 0의 EXT_SLV_SENS_DATA_XX 레지스터에 자동으로 저장되도록 설정할 수 있습니다.
즉, I2C Master모드를 사용해 AK09916 센서의 데이터를 읽는다면 0x28부터 주욱 읽으면 됩니다.

인터럽트

이전 센서와 마찬가지로 인터럽트를 지원합니다. 인터럽트를 활용하면 조금 더 효율적으로 CPU 자원을 아낄 수 있습니다.

센서 주소 선택

센서의 AD0핀의 상태에 따라 두 가지의 센서 주소를 가집니다. AD0핀은 기본적으로 Floating상태이기 때문에 만약 모듈의 AD0에 Pull-resistor가 연결되어있지 않다면 AD0핀을 GND나 VCC중 한곳에 연결해두어야합니다.

목표

I2C FM
DMA+Interrupt로 데이터 읽음
ACC, GYR, MAG 모두 100 Hz로 설정
UART로 PC에 데이터 전송

하드웨어 결선

이제 본격적으로 센서를 사용하기 앞서 결선을 해주겠습니다.
NUCLEO-F103RB
ICM-20948
3V3
VCC
GND
GND
GND
AD0
A5
INT
SCL/D15
SCL
SDA/D14
SDA
누클레오보드의 Arduino 스타일 단자로 나온 I2C핀은 I2C1으로, I2C DMA RX를 사용하게 되면 ST-Link에서 PC와 USB로 연결되는 USART2 DMA TX와 DMA 채널이 겹치게 됩니다. (DMA1 CH7)
헤더 핀으로 나온 I2C2를 활용하면 되기는 하지만,,,귀찮으니 저는 이대로 두겠습니다

CubeMX 세팅

NUCLEO-F103RB의 프리셋을 가져와 필요한 부분만 수정하였습니다.

I2C1

I2C 모드를 Fast Mode로 설정하고 RX모드로 DMA를 설정하였습니다.

USART2

앞서 언급한 내용과 같이 TX DMA를 활용할 수 없기 때문에 일반 모드로 transmit할 예정입니다. 고로 기본값 그대로 두었습니다.

GPIO

NUCLEO 보드의 A5에 해당하는 PC0EXTI로 설정하였습니다. 나중에 IMU의 인터럽트를 Active High로 할 예정이기 때문에 상승 펄스를 읽도록 하였습니다.
핀의 라벨(이름)은 편의를 위해 IMU_INT로 해두었습니다.

NVIC

EXTI line0 인터럽트를 enable해주었습니다.

Clock

HCLK를 최고클럭인 72 MHz로 설정하였습니다.

기타

코드 관리를 위해 몇가지 설정을 바꾸었습니다.
Project
Do not generate the main()
Toolchain / IDE : STM32CubeIDE
Code Generator
Generated files - Generate peripheral initializations as a pair of ‘.c / .h’ files per peripheral

Firmware

CubeMX에서 자동 생성된 코드를 제외한 코드 구성은 아래와 같습니다.
 src
 app
 isr
 isr.c
 app.c
 app.h
 hw
 bsp
 bsp.c
 bsp.h
 driver
 inc
 driver.h
 icm20948.h
 uart.h
 prv
 ak09916_registers.h
 icm20948_registers.h
 driver.c
 icm20948.c
 uart.c
 hw.c
 hw.h
 main.c
코드의 구조에 대한 내용은 제외하고 드라이버에 대한 부분만 살펴보겠습니다.
전체 소스코드는 여기에서 확인할 수 있습니다.
STM32F103_ICM20948_I2C_DMA
varofla

레지스터 정의

센서 내부의 레지스터를 통해 데이터를 읽고 쓰기 때문에 사전에 센서의 레지스터 주소가 기록된 파일이 있으면 편리합니다. 데이터시트를 보며 직접 노가다할까 생각하다가 SparkFun에서 만들어둔 아두이노 라이브러리가 있어 이를 활용하였습니다.
src/hw/driver/prv/ak09916_registers.h
src/hw/driver/prv/icm20948_registers.h

드라이버 해더

출력 데이터 타입

src/hw/driver/inc/icm20948.h
센서 데이터를 얻어오기 위한 데이터 타입을 정의하였습니다. acc(가속도), gyr(자이로), mag(지자기) 타입에 대한 각 축 데이터가 저장되는 타입입니다.

드라이버 초기화

src/hw/driver/inc/icm20948.h
드라이버와 센서를 초기화하는 함수입니다.

데이터 취득

src/hw/driver/inc/icm20948.h
ICM20948 드라이버는 메인 스레드와 무관하게 DMA+Interrupt로 동작하며, 데이터 취득 함수를 통해 가장 최신의 데이터를 가져올 수 있습니다.
반환값은 입력한 버퍼 포인터이며, 데이터가 준비되지 않은 경우 NULL을 반환하도록 하였습니다.

ISR

src/hw/driver/inc/icm20948.h
센서에서 데이터 준비 완료 인터럽트와 DMA Read Finish 인터럽트를 처리하는 각각의 ISR입니다. HAL의 ISR에 이 함수를 추가해 사용합니다.
만약 icm20948의 인터럽트가 아니라면 false를 반환하도록 했습니다.

드라이버 소스

#include

src/hw/driver/icm20948.c
STM32 HAL드라이버의 I2C를 사용하기 위한 i2c.h, icm20948 드라이버의 원형 선언을 담고있는 icm20948.h, 센서 레지스터 선언을 담고있는 ak09916_registers.h, icm20948_registers.h를 include하였습니다.
센서 드라이버와 HAL드라이버 사이의 의존성을 분리하기 위해 i2c 드라이버를 만들어 분리하는게 좋겠지만,, 그건 다음에 하기로 했습니다.

#define

src/hw/driver/icm20948.c
센서의 주소와 I2C 타임아웃을 정의했습니다.
이때 ICM20948의 주소의 LSB는 AD0의 상태에 따라 달라집니다. 저의 경우 AD0을 LOW로 설정해 0xD0을 사용했습니다.
엥 센서 주소가 왜 0x68, 0x69가 아닌가요?
I2C 주소는 7비트로 구성되며, LSB에 따라 R/W가 결정됩니다. 따라서 데이터를 읽거나 쓸 때 shift left한 후 마지막 비트를 1 또는 0으로 OR 연산해야 하는데요,
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_i2c.c
HAL에서는 불친절하게도 shift를 해주지 않아 사용자가 직접 shift해서 넘겨줘야 합니다.
아마 다들 아시겠지만...제 30분을 날린 주범이라 적어봤습니다(...)

(정적) 전역 변수

src/hw/driver/icm20948.c
드라이버 속에서 전역적으로 사용하는 몇몇 변수를 정적 전역 변수로 선언했습니다.
s_dma_buff[22] - DMA RX 버퍼
s_is_data_ready - 새 데이터 존재 여부
s_is_imu_init - imu init 여부

(정적) 전역 함수 - I2C 인터페이스 함수

src/hw/driver/icm20948.c
나름 HAL드라이버와 의존성을 줄이기 위해 HAL을 사용하는 함수는 따로 묶어서 함수로 빼주었습니다.

(정적) 전역 함수 - AK09916 데이터 읽기&쓰기

앞서 언급했듯이 DMA 활용을 위해 AK09916센서에 직접 통신하는것이 아닌, ICM20948을 I2C Master Mode로 설정해 이를 경유하여 읽는다고 하였습니다. 이때문에 AK09916의 레지스터에 접근하기 위해서는 추가적인 루틴이 필요한데요, 이를 위한 함수를 정의하였습니다.
src/hw/driver/icm20948.c
여기서 ICM-20948의 Auxiliary 센서 기능을 조금 알아두어야 합니다.
Auxiliary 센서에서 읽은 데이터는 BANK0EXT_SLV_SENS_DATA_00부터 SLV0~SLV4 순서로 차례로 저장되며, 각 센서 데이터의 시작점은 앞선 I2C_SLVx_CTRL 레지스터의 3:0 값의 합으로 정해집니다. 즉, 만약 첫 번째 센서가 5 len의 데이터를 읽는다면 두 번째 센서에서 읽힌 데이터는 5의 offset을 적용해 EXT_SLV_SENS_DATA_05부터 저장되는것이죠.
여기서는 하나의 Auxiliary 센서(내장 AK09916)만을 사용하기 때문에 I2C_SLV0_xx 레지스터만을 사용합니다.
대략적인 Aux 센서 제어 과정은 아래와 같습니다.
1.
I2C_SLV0_ADDR 레지스터의 6:0에 AK09916의 주소값을 저장, 이때 7번 비트(MSB)에 R/W 비트를 지정
2.
I2C_SLV0_REG 레지스터에 AK09916에서 읽거나 쓸 레지스터 주소를 저장
3.
(데이터를 쓰는 경우) I2C_SLV0_DO 레지스터에 쓸 데이터를 저장
4.
I2C_SLV0_CTRL 레지스터의 3:0에 읽거나 쓸 데이터 길이를, 7번 비트에 1을 저장하여 write
ICM-20948의 I2C Master에서 (해당하는 Auxiliary 센서의 레지스터에 접근해 데이터를 읽거나 씀)
5.
(데이터를 읽는 경우) 읽은 결과가 EXT_SLV_SENS_DATA_00I2C_SLV0_CTRL 레지스터의 3:0(len)만큼 저장됨

(정적) 전역 함수 - 레지스터 뱅크 변경

src/hw/driver/icm20948.c
레지스터 뱅크를 자주 변경해야하기 때문에 이를 함수로 묶어주었습니다.

센서 초기화 함수 (icm20948_init)

여기서부터는 icm20948 드라이버의 API 함수입니다.
src/hw/driver/icm20948.c
우선 레지스터를 읽을 때 사용할 버퍼를 하나 만들고 I2C 통신 함수의 처리결과를 저장할 변수를 선언하였습니다.
src/hw/driver/icm20948.c
그 후 BANK0의 WHO_AM_I 레지스터를 읽어 연결은 올바른지, 센서의 종류는 올바른지 확인하였습니다.
src/hw/driver/icm20948.c
센서가 정상인 것을 확인했다면 센서 세팅에 앞서 센서를 초기화합니다.
src/hw/driver/icm20948.c
클럭과 온도센서를 설정합니다.
CLKSEL[2:0]→0b001 : 클럭 소스 자동 설정 1
TEMP_DIS[3]→1 : 온도센서 끔
src/hw/driver/icm20948.c
ODR align을 켜주었습니다.
src/hw/driver/icm20948.c
자이로와 가속도 센서의 ODR을 설정하였습니다. 두 센서 모두 100 Hz로 설정하였습니다.
추가로 저역필터를 설정하였습니다.
src/hw/driver/icm20948.c
다음으로 I2C Master Mode를 설정합니다. 기능을 리셋해준 후 I2C Master Mode를 Enable하였습니다.
src/hw/driver/icm20948.c
I2C Slave Device의 ODR을 설정하였습니다. 글을 쓰면서 확인했는데, 만약 자이로 센서나 가속도 센서가 켜진 경우 자이로→가속도 순으로 ODR을 따라가기 때문에 I2C_MST_ODR_CONFIG를 따로 설정할 필요가 없다고 하네요.
src/hw/driver/icm20948.c
I2C Slave인 AK09916을 설정합니다. 우선 리셋해준 후 100 Hz 주기로 데이터를 자동으로 읽도록 하였습니다.
그 후 ICM-20948에게 AK09916의 레지스터 데이터를 읽어도록 요청해야하는데요, AK09916의 데이터시트에 따르면 Continuous measurement mode에서는 각 데이터를 읽은 후 ST2를 읽도록 강제하기 때문에 지자기 센서 데이터 레지스터인 HXL~HZH(6 bytes) 이외에도 ST2까지 추가로 2개의 바이트를 더 읽어야 했습니다.
따라서 HXL~ST2까지 총 8개의 바이트를 읽도록 설정하였습니다. (HZH와 ST2 사이에 더미 레지스터가 하나 끼여있습니다)
src/hw/driver/icm20948.c
마지막으로 인터럽트를 켜주었습니다. INT_PIN_CFG 레지스터의 4번 비트를 켜 인터럽트 상태를 초기화한 후 인터럽트를 enable하였습니다.
src/hw/driver/icm20948.c
여기까지 했으면 통신 결과를 확인하고 문제가 없다면 s_is_imu_init 을 true로 저장해둡니다.

데이터 취득 함수 (icm20948_get_data)

src/hw/driver/icm20948.c
Application측에서 icm20948_get_data() 함수를 호출하면 새 데이터의 준비 여부를 확인한 후 IMU 데이터를 버퍼에 쓰고 반환합니다.

IMU 인터럽트 처리

src/hw/driver/icm20948.c
센서로부터 데이터 준비 완료 인터럽트가 발생하면 DMA 컨트롤러에게 I2C 데이터를 읽어오도록 합니다. 이때 읽을 데이터의 길이는 ICM-20948의 데이터인 ACCEL_XOUT_H부터 TEMP_OUT_L까지 총 14바이트 + AK09916의 데이터인 6바이트 = 20바이트입니다.
AK09916의 데이터 갱신을 위해 Master에게 추가적으로 읽도록 한 뒷쪽 2바이트는 읽지 않아도 무방합니다.
src/app/isr/isr.c
ISR에는 위와 같이 등록됩니다.

I2C DMA 인터럽트 처리

src/hw/driver/icm20948.c
데이터가 모두 읽히면 새 데이터가 갱신됨을 드라이버에 저장합니다.
src/app/isr/isr.c
ISR에는 위와 같이 등록됩니다.

센서 드라이버 테스트 예제

구현한 ICM-20948 드라이버를 테스트하기 위해 간단한 Application 코드를 작성하였습니다.
src/app/app.c
uart send 인터페이스인 uart_send_string()의 정의는 아래와 같습니다.
src/hw/driver/uart.c

테스트

RAW 데이터 확인

데이터가 잘 들어오는것을 확인할 수 있었습니다.

필터 적용

출력되는 데이터는 센서의 RAW 데이터이기 때문에 orientation을 얻기 위해서는 bias 설정, fusion filter 적용이 필요합니다.
대충 가속도와 자이로 센서에 오프셋만 적용한 후 두 센서만 상보필터에 넣어 orientation을 구해보았습니다.
잘 작동하네요. 이제 만들어둔 코드를 오래오래 써먹어야겠습니다.