기술 가이드

LIS3DH 가속도 변환: 12비트 부호·BLE·±16g 배율 검증

LIS3DH 고해상도 출력의 부호·BLE·측정 범위 감도를 해독하고, Python으로 524,288개 조합과 잘못된 입력을 검증합니다.

목차

16비트 레지스터를 그대로 가속도로 쓰면 생기는 오류

LIS3DH를 MCU에 연결했는데 정지 상태의 Z축이 예상보다 16배 크게 보이거나, 기울이는 순간 큰 양수로 바뀐다면 전송 오류보다 데이터 표현부터 확인할 수 있습니다. 이 글은 고해상도 모드의 레지스터 해독을 다룹니다. 대상은 STM32·ESP32 등에서 센서 드라이버를 작성하는 펌웨어 개발자입니다.

센서 설정을 읽어 두고, 받은 원시 바이트를 그대로 기록한 다음, 아래 순서로 변환하면 어느 단계에서 값이 달라졌는지 찾기 쉽습니다.

공식 문서에서 확인할 조건

ST LIS3DH 데이터시트의 Table 4·10, 8.11절과 8.16~8.18절을 기준으로 합니다.

  • 고해상도는 LPen=0, HR=1이며 출력은 왼쪽 정렬된 12비트 2의 보수입니다.
  • BLE=0이면 낮은 주소에 LSB, BLE=1이면 MSB가 놓입니다. BLE는 고해상도에서 사용합니다.
  • BDU=1이면 MSB·LSB를 읽기 전까지 출력 레지스터 갱신을 보류합니다.
CTRL_REG4의 FS[1:0] 측정 범위 고해상도 감도(typ.)
00 ±2 g 1 mg/count
01 ±4 g 2 mg/count
10 ±8 g 4 mg/count
11 ±16 g 12 mg/count

±16 g의 배율은 8이 아니라 12입니다. ST 공식 드라이버의 lis3dh_from_fs16_hr_to_mg()도 16비트 입력을 16으로 나눈 뒤 12를 곱합니다. 감도는 전형값이며 보정 오차가 없는 절대 정확도를 뜻하지 않습니다.

부호 복원과 바이트 순서를 따로 확인하기

아래는 BLE=0, ±2 g 설정에서 해독한 산술 예입니다. mg는 milli-g이며 mg / 1000이 g 단위 값입니다.

낮은 주소부터 받은 두 바이트 16비트 조립값 12비트 signed count 변환값
80 3E 0x3E80 1000 1000 mg
80 C1 0xC180 -1000 -1000 mg
F0 FF 0xFFF0 -1 -1 mg
00 80 0x8000 -2048 -2048 mg

마지막 행은 디코더의 수치 경계 시험입니다. 센서가 정격 범위 밖까지 정확하게 측정한다는 의미는 아닙니다. BLE=1에서는 같은 +1000 count가 3E 80 순서로 읽힙니다.

알고리즘은 두 바이트를 설정된 순서로 조립하고, 오른쪽으로 4비트 옮긴 뒤 12비트 부호를 복원합니다. 마지막에 감도를 곱합니다. 이 순서에서는 Python 정수와 MCU의 signed 형변환 규칙을 혼동할 필요가 없습니다. C로 옮길 때도 unsigned 조립 후 명시적 부호 확장을 권합니다.

복사해서 실행하는 Python 검증 예제

Python 3.10 이상에서 아래 전체 코드를 lis3dh_hr_example.py로 저장하고 실행합니다. 외부 패키지나 센서 보드는 필요하지 않습니다.

python -B .\lis3dh_hr_example.py
# Python 3.10+; standard library only
SENSITIVITY_MG = (1, 2, 4, 12)


def axis_mg(pair: bytes, ctrl1: int, ctrl4: int) -> int:
    if len(pair) != 2:
        raise ValueError("one axis needs exactly two bytes")
    if not 0 <= ctrl1 <= 255 or not 0 <= ctrl4 <= 255:
        raise ValueError("register value must be one byte")
    if ctrl1 & 0x08 or not ctrl4 & 0x08:
        raise ValueError("requires LPen=0 and HR=1")
    order = "big" if ctrl4 & 0x40 else "little"
    word = int.from_bytes(pair, order, signed=False)
    count = word >> 4
    if count & 0x800:
        count -= 0x1000
    return count * SENSITIVITY_MG[(ctrl4 >> 4) & 3]


def xyz_mg(frame: bytes, ctrl1: int, ctrl4: int) -> tuple:
    if len(frame) != 6:
        raise ValueError("XYZ needs exactly six bytes")
    return tuple(axis_mg(frame[i:i+2], ctrl1, ctrl4)
                 for i in (0, 2, 4))


def must_reject(fn, *args):
    try:
        fn(*args)
    except ValueError:
        return
    raise AssertionError("invalid input was accepted")


def self_test():
    tested = 0
    for fs, scale in enumerate((1, 2, 4, 12)):
        for ble in (0, 1):
            ctrl4 = 0x88 | (fs << 4) | (ble << 6)
            order = "big" if ble else "little"
            for word in range(65536):
                pair = word.to_bytes(2, order)
                # Independent signed-16-bit arithmetic reference.
                expected = (int.from_bytes(pair, order, signed=True) // 16) * scale
                assert axis_mg(pair, 0x57, ctrl4) == expected
                tested += 1
    assert axis_mg(bytes.fromhex("80 3e"), 0x57, 0x88) == 1000
    assert axis_mg(bytes.fromhex("80 c1"), 0x57, 0x88) == -1000
    assert axis_mg(bytes.fromhex("f0 ff"), 0x57, 0x88) == -1
    assert axis_mg(bytes.fromhex("10 00"), 0x57, 0xb8) == 12
    assert axis_mg(bytes.fromhex("3e 80"), 0x57, 0xc8) == 1000
    assert xyz_mg(bytes.fromhex("80 3e 80 c1 00 00"), 0x57, 0x88) == (1000, -1000, 0)
    for ctrl1, ctrl4 in ((0x57, 0x80), (0x5f, 0x80), (0x5f, 0x88)):
        must_reject(axis_mg, b"\0\0", ctrl1, ctrl4)
    for n in (0, 1, 3):
        must_reject(axis_mg, bytes(n), 0x57, 0x88)
    for n in (0, 5, 7):
        must_reject(xyz_mg, bytes(n), 0x57, 0x88)
    for ctrl1, ctrl4 in ((-1, 0x88), (256, 0x88), (0x57, -1), (0x57, 256)):
        must_reject(axis_mg, b"\0\0", ctrl1, ctrl4)
    print(f"PASS: {tested:,} raw/FS/BLE cases; vectors and invalid inputs")
    print("XYZ mg:", xyz_mg(bytes.fromhex("80 3e 80 c1 00 00"), 0x57, 0x88))


if __name__ == "__main__":
    self_test()

실행 결과:

PASS: 524,288 raw/FS/BLE cases; vectors and invalid inputs
XYZ mg: (1000, -1000, 0)

65,536개의 16비트 입력마다 FS 네 가지와 BLE 두 가지를 검사합니다. 참조 계산은 signed 16비트 정수를 먼저 만든 뒤 16으로 나누므로, 디코더의 12비트 부호 확장과 다른 경로로 비교합니다. 하위 4비트가 0이 아닌 입력도 넣어 버림 규칙을 점검합니다. 그런 비트가 실제 출력에서 유효한 측정 비트라는 뜻은 아닙니다.

실제 MCU에서 읽는 순서

다음은 FIFO·고역 통과 필터·self-test를 사용하지 않는 신규 측정 설정의 출발점입니다. 이미 동작 중인 제품에서는 다른 비트를 보존하고 데이터시트의 모드 전환 대기 시간을 적용합니다.

  1. WHO_AM_I(0x0F)가 0x33인지 확인합니다.
  2. CTRL_REG4(0x23)=0x88로 BDU·HR을 켜고 BLE·FS를 0으로 둡니다. CTRL_REG1(0x20)=0x57은 100 Hz와 XYZ 활성화 예입니다.
  3. 설정을 읽어 되돌림 확인하고, 안정화 후 STATUS_REG(0x27)의 ZYXDA를 기다립니다. 기다림에는 시간 제한을 둡니다.
  4. I²C 연속 읽기는 시작 부주소 0x28 | 0x80 = 0xA8에서 6바이트를 받습니다. 0xA8은 장치 주소가 아닙니다.
  5. 수신 길이·버스 결과를 확인하고, 실제 설정값과 함께 xyz_mg()에 전달합니다.

BDU를 켜도 모든 샘플이 보존되는 것은 아닙니다. 수집 주기가 부족하면 오버런을 기록하고 FIFO·ODR·처리 주기를 함께 검토합니다. 새 설정 직전의 프레임에 새 배율을 붙이지 않도록 설정 변경과 수집 사이도 동기화합니다.

증상별 점검과 검증 범위

증상 먼저 볼 항목
음수가 큰 양수로 표시됨 12비트 부호 확장과 signed/unsigned 혼용
약 16배 차이 왼쪽 정렬을 해제했는지
±16 g에서 배율 불일치 감도를 12로 적용했는지
같은 축이 갑자기 튐 BLE, BDU, 수신 길이, 동시 접근
값이 계속 고정됨 전원·ODR·축 활성화·DRDY·버스 오류

이번 예제의 실행 검증은 호스트에서의 수치 변환과 입력 검사입니다. 실제 보드의 I²C 파형·노이즈·진동·절대 가속도 정확도는 측정하지 않았습니다. 현장에서는 원시 HEX, 설정 레지스터, 수신 결과와 타임스탬프를 함께 남기고 정지·축 반전·알려진 자세에서 비교하세요.

공식 참고 자료

관련 서비스

관련 글

댓글

첫 댓글을 남겨 보세요.

관리자 확인 후 게시됩니다.

처리 중입니다...

잠시만 기다려주세요.