Skip to content

Chapter 12

Hongjayeong-02 edited this page Oct 5, 2026 · 1 revision

12. SPI + ADXL345

⚠️ Hardware Not Verified

이 Chapter는 make를 이용한 Compile / Link까지 확인했다.
ADXL345 sensor module이 없어서 실제 SPI waveform, DEVID = 0xE5, X/Y/Z acceleration 값은 아직 검증하지 않았다.


Goal

STM32F439ZI의 SPI1을 Master mode로 설정하고 ADXL345 accelerometer와 SPI 통신하기 위한 low-level driver를 구현한다.

STM32F439ZI
    │
    │ SPI1
    │
    ├── PA5 → SCK
    ├── PA6 → MISO
    ├── PA7 → MOSI
    └── PA9 → CS
             │
             ▼
          ADXL345

SPI 설정:

SPI      : SPI1
Mode     : Master
SCK      : PA5
MISO     : PA6
MOSI     : PA7
CS       : PA9 GPIO
AF       : AF5
Frame    : 8-bit
Bit Order: MSB First
Mode     : SPI Mode 3
           CPOL = 1
           CPHA = 1

1. SPI 핵심

SPI는 Synchronous Serial Communication 방식이다.

UART와 달리 master가 clock을 직접 생성한다.

Master                         Slave

SCK ───────────────────────→ Clock MOSI ───────────────────────→ Data MISO ←─────────────────────── Data CS ───────────────────────→ Select

각 signal:

SCK
→ Serial Clock

MOSI → Master Out Slave In

MISO → Master In Slave Out

CS → Chip Select


2. SPI1 Pin Mapping

이번 실습:

Pin Function Mode
PA5 SPI1_SCK AF5
PA6 SPI1_MISO AF5
PA7 SPI1_MOSI AF5
PA9 ADXL345 CS GPIO Output

PA5~PA7은:

GPIO Alternate Function
        ↓
AF5
        ↓
SPI1

으로 연결한다.

CS는 SPI hardware NSS를 사용하지 않고 일반 GPIO output으로 직접 제어한다.


3. SPI Registers

이번 코드에서 사용하는 register:

RCC->AHB1ENR
RCC->APB2ENR

GPIOA->MODER GPIOA->AFR[0] GPIOA->ODR

SPI1->CR1 SPI1->SR SPI1->DR

주요 SPI status:

SR.RXNE
→ Receive buffer Not Empty

SR.TXE → Transmit buffer Empty

SR.BSY → SPI communication Busy


4. ADXL345 SPI Command

ADXL345 register access에서는 address의 상위 bit를 command 정보로 사용한다.

bit7
R/W

0 → Write 1 → Read

bit6 MB

0 → Single Byte 1 → Multi Byte

예를 들어 register read command는 개념적으로:

Register Address
OR
0x80
↓
Read Command

Multi-byte read라면:

Register Address
|
0x80
|
0x40

형태가 된다.


5. Practice Code Status

현재 보존된 실습 원본에서 확인되는 파일:

Inc/
└── spi.h

Src/ └── spi.c

ADXL345 application layer는 현재 Notion 및 repository search에서 전체 source를 확인하지 못했다.

따라서 이 Wiki에서는:

spi.h
spi.c

를 confirmed source로 기록한다.

향후 sensor module 확보 후:

adxl345.h
adxl345.c
main.c

를 실제 테스트 코드 기준으로 추가한다.


6. spi.h

#ifndef SPI_H_
#define SPI_H_

#include <stdint.h> #include "stm32f4xx.h"

/*

  • Configure GPIO pins used by SPI1.
  • PA5 → SCK
  • PA6 → MISO
  • PA7 → MOSI
  • PA9 → Software-controlled CS */ void spi_gpio_init(void);

/*

  • Configure SPI1 as Master.
  • ADXL345 communication configuration:
  • CPOL = 1
  • CPHA = 1
  • → SPI Mode 3 */ void spi1_config(void);

/*

  • Transmit one or more bytes through SPI1.
  • data:
  • pointer to TX buffer
    
  • size:
  • number of bytes to send
    

*/ void spi1_transmit(uint8_t *data, uint32_t size);

/*

  • Receive one or more bytes through SPI1.
  • SPI master must generate SCK even when only
  • receiving data, therefore the implementation
  • transmits dummy bytes internally. */ void spi1_receive(uint8_t *data, uint32_t size);

/*

  • Assert chip select.
  • ADXL345 CS is active LOW. */ void cs_enable(void);

/*

  • Deassert chip select. */ void cs_disable(void);

#endif


7. spi.c

#include "spi.h"

/*

  • ============================================================
  • RCC Clock Enable Bits
  • ============================================================ */

/*

  • RCC AHB1ENR bit0
  • GPIOA clock enable.
  • SPI1 pins PA5 / PA6 / PA7 and
  • software CS PA9 all belong to GPIOA. */ #define GPIOAEN (1U << 0)

/*

  • RCC APB2ENR bit12
  • SPI1 peripheral clock enable. */ #define SPI1EN (1U << 12)

/*

  • ============================================================
  • SPI Status Register Bits
  • ============================================================ */

/*

  • SR bit0
  • RXNE = Receive buffer Not Empty
  • 0 → no new received data
  • 1 → received data available in DR */ #define SR_RXNE (1U << 0)

/*

  • SR bit1
  • TXE = Transmit buffer Empty
  • 0 → transmit buffer not ready
  • 1 → new data may be written to DR */ #define SR_TXE (1U << 1)

/*

  • SR bit7
  • BSY = Busy flag
  • 1 → SPI communication still in progress
  • 0 → SPI bus no longer busy */ #define SR_BSY (1U << 7)

/*

  • ============================================================
  • SPI GPIO Initialization
  • ============================================================ */

void spi_gpio_init(void) { /* * -------------------------------------------------------- * Step 1. Enable GPIOA Clock * -------------------------------------------------------- */ RCC->AHB1ENR |= GPIOAEN;

/*
 * --------------------------------------------------------
 * Step 2. PA5 → Alternate Function
 * --------------------------------------------------------
 *
 * PA5:
 *
 *     SPI1_SCK
 *
 * MODER<span>[11:10]</span> = 10
 */
GPIOA-&gt;MODER &amp;= ~(0x3UL &lt;&lt; (5 * 2));
GPIOA-&gt;MODER |=  (0x2UL &lt;&lt; (5 * 2));


/*
 * --------------------------------------------------------
 * Step 3. PA6 → Alternate Function
 * --------------------------------------------------------
 *
 * PA6:
 *
 *     SPI1_MISO
 *
 * MODER<span>[13:12]</span> = 10
 */
GPIOA-&gt;MODER &amp;= ~(0x3UL &lt;&lt; (6 * 2));
GPIOA-&gt;MODER |=  (0x2UL &lt;&lt; (6 * 2));


/*
 * --------------------------------------------------------
 * Step 4. PA7 → Alternate Function
 * --------------------------------------------------------
 *
 * PA7:
 *
 *     SPI1_MOSI
 *
 * MODER<span>[15:14]</span> = 10
 */
GPIOA-&gt;MODER &amp;= ~(0x3UL &lt;&lt; (7 * 2));
GPIOA-&gt;MODER |=  (0x2UL &lt;&lt; (7 * 2));


/*
 * --------------------------------------------------------
 * Step 5. PA9 → General Purpose Output
 * --------------------------------------------------------
 *
 * PA9 is NOT using hardware NSS.
 *
 * It is manually controlled as the ADXL345
 * Chip Select signal.
 *
 * MODER<span>[19:18]</span> = 01
 */
GPIOA-&gt;MODER &amp;= ~(0x3UL &lt;&lt; (9 * 2));
GPIOA-&gt;MODER |=  (0x1UL &lt;&lt; (9 * 2));


/*
 * --------------------------------------------------------
 * Step 6. PA5 → AF5
 * --------------------------------------------------------
 *
 * PA5 is in pin range 0~7,
 * therefore AFR<span>[0]</span> is used.
 *
 * Each pin uses 4 bits.
 */
GPIOA-&gt;AFR<span>[0]</span> &amp;= ~(0xFUL &lt;&lt; (5 * 4));
GPIOA-&gt;AFR<span>[0]</span> |=  (0x5UL &lt;&lt; (5 * 4));


/*
 * --------------------------------------------------------
 * Step 7. PA6 → AF5
 * --------------------------------------------------------
 */
GPIOA-&gt;AFR<span>[0]</span> &amp;= ~(0xFUL &lt;&lt; (6 * 4));
GPIOA-&gt;AFR<span>[0]</span> |=  (0x5UL &lt;&lt; (6 * 4));


/*
 * --------------------------------------------------------
 * Step 8. PA7 → AF5
 * --------------------------------------------------------
 */
GPIOA-&gt;AFR<span>[0]</span> &amp;= ~(0xFUL &lt;&lt; (7 * 4));
GPIOA-&gt;AFR<span>[0]</span> |=  (0x5UL &lt;&lt; (7 * 4));

}

/*

  • ============================================================
  • SPI1 Configuration
  • ============================================================ */

void spi1_config(void) { /* * -------------------------------------------------------- * Step 1. Enable SPI1 Clock * -------------------------------------------------------- * * SPI1 belongs to APB2. */ RCC->APB2ENR |= SPI1EN;

/*
 * --------------------------------------------------------
 * Step 2. Configure SPI Clock Divider
 * --------------------------------------------------------
 *
 * CR1 BR<span>[2:0]</span>
 *
 * Current setting:
 *
 *     001
 *
 * → fPCLK / 4
 */

/*
 * BR0 = 1
 */
SPI1-&gt;CR1 |= (1U &lt;&lt; 3);

/*
 * BR1 = 0
 */
SPI1-&gt;CR1 &amp;= ~(1U &lt;&lt; 4);

/*
 * BR2 = 0
 */
SPI1-&gt;CR1 &amp;= ~(1U &lt;&lt; 5);


/*
 * --------------------------------------------------------
 * Step 3. Configure SPI Mode 3
 * --------------------------------------------------------
 *
 * ADXL345 configuration used in this practice:
 *
 *     CPOL = 1
 *     CPHA = 1
 *
 * SPI Mode 3
 */

/*
 * CPHA = 1
 */
SPI1-&gt;CR1 |= (1U &lt;&lt; 0);

/*
 * CPOL = 1
 */
SPI1-&gt;CR1 |= (1U &lt;&lt; 1);


/*
 * --------------------------------------------------------
 * Step 4. Full-Duplex Mode
 * --------------------------------------------------------
 *
 * BIDIMODE = 0
 *
 * Separate MOSI and MISO lines are used.
 */
SPI1-&gt;CR1 &amp;= ~(1U &lt;&lt; 10);


/*
 * --------------------------------------------------------
 * Step 5. MSB First
 * --------------------------------------------------------
 *
 * LSBFIRST = 0
 *
 * Most Significant Bit is transmitted first.
 */
SPI1-&gt;CR1 &amp;= ~(1U &lt;&lt; 7);


/*
 * --------------------------------------------------------
 * Step 6. Master Mode
 * --------------------------------------------------------
 *
 * MSTR = 1
 *
 * STM32 generates SCK.
 */
SPI1-&gt;CR1 |= (1U &lt;&lt; 2);


/*
 * --------------------------------------------------------
 * Step 7. 8-bit Data Frame
 * --------------------------------------------------------
 *
 * DFF = 0
 *
 * Each SPI transfer uses 8-bit data.
 */
SPI1-&gt;CR1 &amp;= ~(1U &lt;&lt; 11);


/*
 * --------------------------------------------------------
 * Step 8. Software Slave Management
 * --------------------------------------------------------
 *
 * SSM = 1
 *
 * Hardware NSS management is disabled.
 *
 * PA9 GPIO is used as CS instead.
 */
SPI1-&gt;CR1 |= (1U &lt;&lt; 8);


/*
 * --------------------------------------------------------
 * Step 9. Internal Slave Select
 * --------------------------------------------------------
 *
 * SSI = 1
 *
 * Required with software slave management
 * so the master does not enter mode-fault state.
 */
SPI1-&gt;CR1 |= (1U &lt;&lt; 9);


/*
 * --------------------------------------------------------
 * Step 10. Enable SPI1
 * --------------------------------------------------------
 *
 * SPE = 1
 */
SPI1-&gt;CR1 |= (1U &lt;&lt; 6);

}

/*

  • ============================================================
  • SPI Transmit
  • ============================================================ */

void spi1_transmit(uint8_t *data, uint32_t size) { uint32_t i = 0; uint8_t temp;

/*
 * Send every byte in the TX buffer.
 */
while (i &lt; size)
{
    /*
     * Wait until transmit buffer becomes empty.
     *
     * TXE = 1
     *
     * means SPI1-&gt;DR can accept new data.
     */
    while (!(SPI1-&gt;SR &amp; SR_TXE))
    {
    }


    /*
     * Write one byte to Data Register.
     *
     * SPI hardware now shifts this byte
     * out through MOSI.
     */
    SPI1-&gt;DR = data<span>[i]</span>;


    /*
     * Move to next byte.
     */
    i++;
}


/*
 * Wait until the final value has moved
 * out of the transmit buffer.
 */
while (!(SPI1-&gt;SR &amp; SR_TXE))
{
}


/*
 * TXE only tells us the transmit buffer is empty.
 *
 * The SPI shift register may still be transferring
 * the final bits on the physical bus.
 *
 * Therefore also wait until:
 *
 *     BSY = 0
 */
while (SPI1-&gt;SR &amp; SR_BSY)
{
}


/*
 * --------------------------------------------------------
 * Clear possible OVR condition
 * --------------------------------------------------------
 *
 * SPI is full-duplex.
 *
 * Even while transmitting, received data may
 * accumulate in the receive side.
 *
 * The required clear sequence is:
 *
 *     Read DR
 *       ↓
 *     Read SR
 *
 * The actual value is not needed.
 */
temp = SPI1-&gt;DR;
temp = SPI1-&gt;SR;

/*
 * Prevent compiler warning:
 *
 * variable intentionally read only for
 * hardware side effect.
 */
(void)temp;

}

/*

  • ============================================================
  • SPI Receive
  • ============================================================ */

void spi1_receive(uint8_t data, uint32_t size) { while (size) { / * ---------------------------------------------------- * Generate SPI Clock * ---------------------------------------------------- * * SPI slave cannot send data by itself. * * SCK is generated only by the master. * * Therefore even when we only want to receive, * the master must transmit a dummy byte. * * The actual dummy data value is irrelevant here. */ SPI1->DR = 0;

    /*
     * Wait until a byte has been received.
     *
     * RXNE:
     *
     *     0 → no received byte
     *     1 → received byte available
     */
    while (!(SPI1-&gt;SR &amp; SR_RXNE))
    {
    }


    /*
     * Read received byte from Data Register
     * and store it in the caller's buffer.
     */
    *data++ = (uint8_t)SPI1-&gt;DR;


    /*
     * One byte completed.
     */
    size--;
}

}

/*

  • ============================================================
  • Chip Select Control
  • ============================================================ */

void cs_enable(void) { /* * ADXL345 chip select is active LOW. * * PA9 = 0 * * → Sensor selected * → SPI transaction begins */ GPIOA->ODR &= ~(1U << 9); }

void cs_disable(void) { /* * PA9 = 1 * * → Sensor deselected * → SPI transaction ends */ GPIOA->ODR |= (1U << 9); }


8. SPI Configuration Flow

spi_gpio_init()
│
├── GPIOA Clock ON
├── PA5 → AF Mode
├── PA6 → AF Mode
├── PA7 → AF Mode
├── PA9 → Output
└── PA5/6/7 → AF5

spi1_config() │ ├── SPI1 Clock ON ├── BR = /4 ├── CPOL = 1 ├── CPHA = 1 ├── Full Duplex ├── MSB First ├── Master ├── 8-bit ├── SSM = 1 ├── SSI = 1 └── SPE = 1


9. SPI Transmit Flow

TX Buffer
↓
Wait TXE
↓
Write SPI1->DR
↓
Shift Register
↓
MOSI
↓
ADXL345

마지막 byte까지 넣었다고 바로 transaction이 끝난 것은 아니다.

TXE = 1

은 transmit buffer가 비었다는 뜻이고:

BSY = 0

까지 확인해야 실제 bus transfer 완료를 확인할 수 있다.

그래서:

while (!(SPI1->SR & SR_TXE))
{
}

while (SPI1->SR & SR_BSY) { }

두 상태를 모두 확인한다.


10. Why Dummy Write?

SPI에서는 master가 SCK를 만든다.

따라서 slave data만 받고 싶어도:

Master
↓
Generate Clock
↓
Slave shifts MISO data

과정이 필요하다.

코드:

SPI1->DR = 0;

의 0은 의미 있는 command data가 아니라 clock을 발생시키기 위한 dummy transmit이다.

Dummy TX
↓
SCK generated
↓
Slave shifts data
↓
RXNE = 1
↓
Read DR

11. Full-Duplex

SPI는 기본적으로 transmit과 receive가 동시에 진행된다.

SCK edge
│
├── MOSI bit → Slave
│
└── MISO bit → Master

즉:

Transmit only

처럼 코드를 작성해도 hardware에서는 receive path가 같이 동작할 수 있다.

그래서 사용하지 않은 receive data를 처리하지 않으면:

OVR
Overrun

상태가 발생할 수 있다.


12. OVR Clear

원본 코드:

temp = SPI1->DR;
temp = SPI1->SR;
(void)temp;

이 부분은 값을 계산하기 위해 존재하는 것이 아니다.

Read DR
↓
Read SR
↓
OVR clear

라는 hardware-defined side effect를 사용한다.

즉 bare-metal에서는:

register를 읽는 행위 자체가 hardware state를 변경할 수 있다.

는 점이 중요하다.


13. Chip Select

SPI transaction의 기본 경계:

CS HIGH
↓
CS LOW
↓
Command
↓
Data
↓
CS HIGH

코드에서는:

cs_enable();

/* SPI communication */

cs_disable();

형태로 사용한다.


14. Why GPIO CS?

이번 코드는 hardware NSS가 아니라:

PA9 GPIO

를 CS로 사용한다.

그리고 SPI 내부에서는:

SSM = 1
SSI = 1

로 software slave management를 사용한다.

이렇게 하면 application에서:

Sensor A CS
Sensor B CS
Sensor C CS

처럼 여러 slave를 각 GPIO로 제어하기 쉽다.


15. ADXL345 Verification Target

⚠️ 다음 내용은 아직 실제 측정 결과가 아니라 재실습 시 확인해야 할 목표값이다.

첫 번째 검증은 acceleration data보다:

DEVID

를 먼저 읽는 것이 좋다.

Expected:

ADXL345 DEVID

0xE5

즉:

STM32 SPI 설정
+
배선
+
CS
+
SPI Mode
+
Sensor 응답

이 모두 정상인지 가장 간단하게 확인할 수 있다.


16. Application Layer Status

원래 Chapter 목표에는:

ADXL345 DEVID Read
X/Y/Z Acceleration Read
UART Output

까지 포함되어 있다.

하지만 현재 보존된 source에서 전체 내용을 확인할 수 있는 것은:

spi.h
spi.c

까지다.

따라서 현재 Wiki에서는 application code를 추측해서 원본처럼 만들지 않는다.

재실습 후:

adxl345.h
adxl345.c
main.c

를 실제 실행했던 source 그대로 추가한다.


17. Build Result

현재 확인된 결과:

Compile : PASS
Link    : PASS
Hardware: NOT VERIFIED

arm-none-eabi-size:

text = 2328 bytes
data = 0 bytes
bss  = 2560 bytes

경고 없이 build가 완료되었다.


18. Hardware Re-Test Plan

ADXL345 module 확보 후 다음 순서로 검증한다.

Step 1 — Wiring

STM32F439ZI       ADXL345

PA5 SCK → SCLK PA6 MISO ← SDO PA7 MOSI → SDA/SDI PA9 CS → CS

3.3V → VCC GND → GND

실제 module pin label과 전원 사양은 사용 모듈의 datasheet/board schematic을 다시 확인한다.


Step 2 — Debug GPIO

확인:

PA5 MODER → AF
PA6 MODER → AF
PA7 MODER → AF

PA5 AF → 5 PA6 AF → 5 PA7 AF → 5

PA9 MODER → Output


Step 3 — Debug SPI1

확인:

SPI1->CR1

Expected:

MSTR = 1
CPOL = 1
CPHA = 1
DFF  = 0
SSM  = 1
SSI  = 1
SPE  = 1

Step 4 — Check CS

Idle:

PA9 = HIGH

Transaction:

PA9 = LOW

완료:

PA9 = HIGH

Step 5 — Read DEVID

First hardware target:

DEVID == 0xE5

결과가 다르면 먼저:

Wiring
SPI Mode
CS polarity
MOSI/MISO swap
AF setting
Clock rate

을 확인한다.


Step 6 — Read X/Y/Z

DEVID가 정상일 때 그다음:

DATAX
DATAY
DATAZ

multi-byte read를 검증한다.


19. Screenshots to Add

현재 넣을 수 있는 것:

images/ch12_build.png
images/ch12_size.png

실기 후 추가:

images/ch12_wiring.jpg
images/ch12_spi_registers.png
images/ch12_devid.png
images/ch12_logic_analyzer.png
images/ch12_xyz_uart.png

특히 가장 중요한 실기 증거:

DEVID = 0xE5

와 가능하면:

CS
SCK
MOSI
MISO

logic analyzer waveform이다.


20. Key Points

SPI1 Master
│
├── PA5 → SCK
├── PA6 ← MISO
├── PA7 → MOSI
└── PA9 → CS
↓
ADXL345
  • SPI는 master가 SCK를 생성하는 synchronous serial protocol이다.

  • SPI1의 PA5/PA6/PA7은 AF5를 사용한다.

  • PA9는 software-controlled CS로 사용한다.

  • MSTR=1로 Master mode를 설정한다.

  • 이번 실습은 CPOL=1, CPHA=1인 Mode 3를 사용한다.

  • SSM=1, SSI=1로 software slave management를 사용한다.

  • TXE는 transmit buffer empty를 의미한다.

  • RXNE는 received data가 있다는 뜻이다.

  • BSY는 실제 SPI transfer가 진행 중임을 나타낸다.

  • SPI receive에서도 master가 clock을 만들기 위해 dummy byte를 transmit해야 한다.

  • OVR clear에는 DR read → SR read sequence가 사용된다.

  • 현재 상태는 build/link PASS, hardware verification pending이다.

  • 첫 실기 확인 목표는 **ADXL345 DEVID = 0xE5**다.

Clone this wiki locally