STM32 LibOpenCM3:SPI (Master mode)

系列:簡單入門 LibOpenCM3 STM32 嵌入式系統開發 Posted on 2022-10-07

前言

SPI(Serial Peripheral Interface)是一種常見的同步序列通訊協定,爲主從式架構。有許多感測器或模組都使用 SPI 進行通訊。

這次的範例要實現 USART 與 SPI (Master mode) 的轉發器——把 USART 接收到的資料由 SPI 發送出去,而 SPI 收到的資料由 USART 發送。並且有一個 EXTI 的外部請求接腳。

最典型的 SPI 有 4 條線:

  • SCK:Serial clock
  • MOSI:Master output, slave input
  • MISO:Master input, slave output
  • SS:Slave select,或 CS(Chip select)

關於 SPI 本身我並不打算詳細介紹,若讀者還不熟悉 SPI 的基本概念的話,建議先另外查詢相關文章。我覺得「Day 13:SPI (Part 1) - 原來是 Shift Register 啊!我還以為是 SPI 呢!」與「SPI (Serial Peripheral Interface) 串列 (序列) 週邊介面」這兩篇寫得就很不錯。

正文

首先一樣以 Nucleo-F446RE 做示範。

首先建立一個 PIO 的專案,選擇 Framework 爲「libopencm3」,並在 src/ 資料夾中新增並開啓 main.cmain.h

完整程式

  1/**
  2 * @file   main.c
  3 * @brief  SPI master mode example for STM32 Nucleo-F446RE.
  4 */
  5
  6#include "main.h"
  7
  8int main(void)
  9{
 10  rcc_setup();
 11  usart_setup();
 12  spi_setup();
 13  spi_rq_setup();
 14
 15  usart_send_blocking(USART2, 'M');
 16  usart_send_blocking(USART2, 'a');
 17  usart_send_blocking(USART2, 's');
 18  usart_send_blocking(USART2, 't');
 19  usart_send_blocking(USART2, 'e');
 20  usart_send_blocking(USART2, 'r');
 21  usart_send_blocking(USART2, '\r');
 22  usart_send_blocking(USART2, '\n');
 23
 24  while (1)
 25  { }
 26  return 0;
 27}
 28
 29static void spi_select(void)
 30{
 31  gpio_clear(GPIO_SPI_PORT, GPIO_SPI_CS_PIN);
 32}
 33
 34static void spi_deselect(void)
 35{
 36  gpio_set(GPIO_SPI_PORT, GPIO_SPI_CS_PIN);
 37}
 38
 39static void rcc_setup(void)
 40{
 41  rcc_clock_setup_pll(&rcc_hse_8mhz_3v3[RCC_CLOCK_3V3_168MHZ]);
 42
 43  rcc_periph_clock_enable(RCC_SYSCFG); /* For EXTI. */
 44  rcc_periph_clock_enable(RCC_GPIOA);
 45  rcc_periph_clock_enable(RCC_GPIOC);
 46  rcc_periph_clock_enable(RCC_USART2);
 47  rcc_periph_clock_enable(RCC_SPI1);
 48}
 49
 50static void spi_setup(void)
 51{
 52  /*
 53   * Set SPI-SCK & MISO & MOSI pin to alternate function.
 54   * Set SPI-CS pin to output push-pull (control CS by manual).
 55   */
 56  gpio_mode_setup(GPIO_SPI_PORT,
 57                  GPIO_MODE_AF,
 58                  GPIO_PUPD_NONE,
 59                  GPIO_SPI_SCK_PIN | GPIO_SPI_MISO_PIN | GPIO_SPI_MOSI_PIN);
 60
 61  gpio_set_output_options(GPIO_SPI_PORT,
 62                          GPIO_OTYPE_PP,
 63                          GPIO_OSPEED_50MHZ,
 64                          GPIO_SPI_SCK_PIN | GPIO_SPI_MOSI_PIN);
 65
 66  gpio_set_af(GPIO_SPI_PORT,
 67              GPIO_SPI_AF,
 68              GPIO_SPI_SCK_PIN | GPIO_SPI_MISO_PIN | GPIO_SPI_MOSI_PIN);
 69
 70  /* In master mode, control CS by user instead of AF. */
 71  gpio_mode_setup(GPIO_SPI_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_SPI_CS_PIN);
 72  gpio_set_output_options(GPIO_SPI_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_25MHZ, GPIO_SPI_CS_PIN);
 73
 74  spi_disable(SPI1);
 75  spi_reset(SPI1);
 76
 77  /* Set up in master mode. */
 78  spi_init_master(SPI1,
 79                  SPI_CR1_BAUDRATE_FPCLK_DIV_64,   /* Clock baudrate. */
 80                  SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE, /* CPOL = 0. */
 81                  SPI_CR1_CPHA_CLK_TRANSITION_2,   /* CPHA = 1. */
 82                  SPI_CR1_DFF_8BIT,                /* Data frame format. */
 83                  SPI_CR1_MSBFIRST);               /* Data frame bit order. */
 84  spi_set_full_duplex_mode(SPI1);
 85
 86  /*
 87   * CS pin is not used on master side at standard multi-slave config.
 88   * It has to be managed internally (SSM=1, SSI=1)
 89   * to prevent any MODF error.
 90   */
 91  spi_enable_software_slave_management(SPI1); /* SSM = 1. */
 92  spi_set_nss_high(SPI1);                     /* SSI = 1. */
 93
 94  spi_deselect();
 95  spi_enable(SPI1);
 96}
 97
 98static void spi_rq_setup(void)
 99{
100  /* Set RQ pin to input floating. */
101  gpio_mode_setup(GPIO_SPI_RQ_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_SPI_RQ_PIN);
102
103  /* Setup interrupt. */
104  exti_select_source(EXTI_SPI_RQ, GPIO_SPI_RQ_PORT);
105  exti_set_trigger(EXTI_SPI_RQ, EXTI_TRIGGER_FALLING);
106  exti_enable_request(EXTI_SPI_RQ);
107  nvic_enable_irq(NVIC_SPI_RQ_IRQ);
108}
109
110static void usart_setup(void)
111{
112  /* Set USART-Tx & Rx pin to alternate function. */
113  gpio_mode_setup(GPIO_USART_TXRX_PORT,
114                  GPIO_MODE_AF,
115                  GPIO_PUPD_NONE,
116                  GPIO_USART_TX_PIN | GPIO_USART_RX_PIN);
117
118  gpio_set_af(GPIO_USART_TXRX_PORT,
119              GPIO_USART_AF,
120              GPIO_USART_TX_PIN | GPIO_USART_RX_PIN);
121
122  /* Setup interrupt. */
123  nvic_enable_irq(NVIC_USART2_IRQ);
124  usart_enable_rx_interrupt(USART2); /* Enable receive interrupt. */
125
126  /* Config USART params. */
127  usart_set_baudrate(USART2, USART_BAUDRATE);
128  usart_set_databits(USART2, 8);
129  usart_set_stopbits(USART2, USART_STOPBITS_1);
130  usart_set_parity(USART2, USART_PARITY_NONE);
131  usart_set_flow_control(USART2, USART_FLOWCONTROL_NONE);
132  usart_set_mode(USART2, USART_MODE_TX_RX);
133
134  usart_enable(USART2);
135}
136
137/**
138 * @brief USART2 Interrupt service routine.
139 */
140void usart2_isr(void)
141{
142  uint8_t indata = usart_recv(USART2); /* Read received data. */
143
144  spi_select();
145  spi_send(SPI1, indata);
146
147  /* Wait for SPI transmit complete. */
148  while (!(SPI_SR(SPI1) & SPI_SR_TXE)) /* Wait for 'Transmit buffer empty' flag to set. */
149  { }
150  while ((SPI_SR(SPI1) & SPI_SR_BSY)) /* Wait for 'Busy' flag to reset. */
151  { }
152
153  spi_deselect();
154
155  /* Clear 'Read data register not empty' flag. */
156  USART_SR(USART2) &= ~USART_SR_RXNE;
157}
158
159/**
160 * @brief EXTI9~5 Interrupt service routine.
161 */
162void exti9_5_isr(void)
163{
164  exti_reset_request(EXTI_SPI_RQ);
165
166  spi_select();
167  spi_send(SPI1, 0x00);               /* Just for beget clock signal. */
168  while ((SPI_SR(SPI1) & SPI_SR_BSY)) /* Wait for 'Busy' flag to reset. */
169  { }
170  uint8_t indata = spi_read(SPI1);
171
172  while ((SPI_SR(SPI1) & SPI_SR_BSY)) /* Wait for 'Busy' flag to reset. */
173  { }
174  spi_deselect();
175
176  usart_send_blocking(USART2, indata);
177}
 1/** 
 2 * @file main.h
 3 */
 4
 5#ifndef MAIN_H
 6#define MAIN_H
 7
 8#include <libopencm3/stm32/rcc.h>
 9#include <libopencm3/stm32/gpio.h>
10#include <libopencm3/stm32/spi.h>
11#include <libopencm3/stm32/usart.h>
12#include <libopencm3/stm32/exti.h>
13#include <libopencm3/cm3/nvic.h>
14
15#define USART_BAUDRATE (9600)
16
17#define GPIO_SPI_PORT (GPIOA)
18#define GPIO_SPI_SCK_PIN (GPIO5)  /* D13. */
19#define GPIO_SPI_MISO_PIN (GPIO6) /* D12. */
20#define GPIO_SPI_MOSI_PIN (GPIO7) /* D11. */
21#define GPIO_SPI_CS_PIN (GPIO4)   /* A2. */
22#define GPIO_SPI_AF (GPIO_AF5)    /* Ref: Table-11 in DS10693. */
23
24#define GPIO_SPI_RQ_PORT (GPIOC)
25#define GPIO_SPI_RQ_PIN (GPIO7) /* D9. */
26#define EXTI_SPI_RQ (EXTI7)
27#define NVIC_SPI_RQ_IRQ (NVIC_EXTI9_5_IRQ)
28
29#define GPIO_USART_TXRX_PORT (GPIOA)
30#define GPIO_USART_TX_PIN (GPIO2) /* ST-Link (D1). */
31#define GPIO_USART_RX_PIN (GPIO3) /* ST-Link (D0). */
32#define GPIO_USART_AF (GPIO_AF7)  /* Ref: Table-11 in DS10693. */
33
34static void usart_setup(void);
35static void spi_rq_setup(void);
36static void spi_setup(void);
37static void rcc_setup(void);
38
39static void spi_select(void);
40static void spi_deselect(void);
41
42#endif /* MAIN_H. */

分段說明

Include

1// main.h
2#include <libopencm3/stm32/rcc.h>
3#include <libopencm3/stm32/gpio.h>
4#include <libopencm3/stm32/spi.h>
5#include <libopencm3/stm32/usart.h>
6#include <libopencm3/stm32/exti.h>
7#include <libopencm3/cm3/nvic.h>

除了基本的 rcc.hgpio.h,這次的 spi.husart.hnvic.h 外,我希望此 SPI 有一個獨立的 EXTI 請求接腳,所以還會用到 exti.h

設定 SPI

 1
 2static void spi_setup(void)
 3{
 4  /*
 5   * Set SPI-SCK & MISO & MOSI pin to alternate function.
 6   * Set SPI-CS pin to output push-pull (control CS by manual).
 7   */
 8  gpio_mode_setup(GPIO_SPI_PORT,
 9                  GPIO_MODE_AF,
10                  GPIO_PUPD_NONE,
11                  GPIO_SPI_SCK_PIN | GPIO_SPI_MISO_PIN | GPIO_SPI_MOSI_PIN);
12
13  gpio_set_output_options(GPIO_SPI_PORT,
14                          GPIO_OTYPE_PP,
15                          GPIO_OSPEED_50MHZ,
16                          GPIO_SPI_SCK_PIN | GPIO_SPI_MOSI_PIN);
17
18  gpio_set_af(GPIO_SPI_PORT,
19              GPIO_SPI_AF,
20              GPIO_SPI_SCK_PIN | GPIO_SPI_MISO_PIN | GPIO_SPI_MOSI_PIN);
21
22  /* In master mode, control CS by user instead of AF. */
23  gpio_mode_setup(GPIO_SPI_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_SPI_CS_PIN);
24  gpio_set_output_options(GPIO_SPI_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_25MHZ, GPIO_SPI_CS_PIN);
25
26  spi_disable(SPI1);
27  spi_reset(SPI1);
28
29  /* Set up in master mode. */
30  spi_init_master(SPI1,
31                  SPI_CR1_BAUDRATE_FPCLK_DIV_64,   /* Clock baudrate. */
32                  SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE, /* CPOL = 0. */
33                  SPI_CR1_CPHA_CLK_TRANSITION_2,   /* CPHA = 1. */
34                  SPI_CR1_DFF_8BIT,                /* Data frame format. */
35                  SPI_CR1_MSBFIRST);               /* Data frame bit order. */
36  spi_set_full_duplex_mode(SPI1);
37
38  /*
39   * CS pin is not used on master side at standard multi-slave config.
40   * It has to be managed internally (SSM=1, SSI=1)
41   * to prevent any MODF error.
42   */
43  spi_enable_software_slave_management(SPI1); /* SSM = 1. */
44  spi_set_nss_high(SPI1);                     /* SSI = 1. */
45
46  spi_deselect();
47  spi_enable(SPI1);
48}

首先要設定 SPI 的 GPIO。除了 CS 腳設定爲通用功能 Push-Pull 輸出模式外,SCK、MOSI 與 MISO 都設定成 Alternate function Push-Pull。

再來是設定 SPI 本身。在使用 SPI 通訊時有幾個比較重要的設定要注意,首先是 SPI Mode,也就是 CPOL(Clock Polarity) 與 CPHA(Clock Phase) 的設定。

CPOL 決定了 SPI 閒置時 SCK 要爲 Low(CPOL = 0) 還是 High(CPOL = 1);CPHA 則是定義 SPI 的資料取樣要在第 1 個邊緣(CPHA = 0),還是第 2 個邊緣(CPHA = 1)。因此共有 4 種組合:

ModeCPOLCPHA
000
101
210
311

這裡我使用 CPOL = 0SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE)與 CPHA = 1SPI_CR1_CPHA_CLK_TRANSITION_2),也就是 Mode 1。根據此設定,因爲閒置時 SCK 是 Low,而 SPI 在第 2 個邊緣進行資料取樣,也就是在 SCK 的負緣採樣。

另外使用 spi_set_full_duplex_mode() 將 SPI 設爲全雙工模式。

要注意的是,若是使用一般的 SPI 配置(一個 Master,多個 Slave)的話,Master device 的 CS(NSS)腳是沒特殊作用的(即 AF 不會控制它,要使用者自己手動控制),且要啓用「Software NSS management(SSM=1)」和將 SSI(Internal slave select)設爲 1,以避免出錯。因此呼叫 spi_enable_software_slave_management()spi_set_nss_high()

NSS pin is not used on master side at this configuration. It has to be managed internally (SSM=1, SSI=1) to prevent any MODF error. 參考自 RM0390 Rev6 P.852。

▲ Standard multi-slave communication 的 SPI 接線圖。取自 RM0390 Rev6 P.852

▲ Standard multi-slave communication 的 SPI 接線圖。取自 RM0390 Rev6 P.852

SPI CS 選擇/反選擇

1static void spi_select(void)
2{
3  gpio_clear(GPIO_SPI_CS_PORT, GPIO_SPI_CS_PIN);
4}
5
6static void spi_deselect(void)
7{
8  gpio_set(GPIO_SPI_CS_PORT, GPIO_SPI_CS_PIN);
9}

CS 的控制就是一般的 GPIO 輸出,將其寫成函式以方便操作。

USART ISR

 1/**
 2 * @brief USART2 Interrupt service routine.
 3 */
 4void usart2_isr(void)
 5{
 6  uint8_t indata = usart_recv(USART2); /* Read received data. */
 7
 8  spi_select();
 9  spi_send(SPI1, indata);
10
11  /* Wait for SPI transmit complete. */
12  while (!(SPI_SR(SPI1) & SPI_SR_TXE)) /* Wait for 'Transmit buffer empty' flag to set. */
13  { }
14  while ((SPI_SR(SPI1) & SPI_SR_BSY)) /* Wait for 'Busy' flag to reset. */
15  { }
16
17  spi_deselect();
18
19  /* Clear 'Read data register not empty' flag. */
20  USART_SR(USART2) &= ~USART_SR_RXNE;
21}

由於目標功能是 USART-SPI 的轉發器,所以在 USART 接收到資料後,要將接收到的資料透過 SPI 傳送出去。

這裡的 SPI 傳送步驟爲:

  1. 選擇 Slave device(CS 輸出 Low)。
  2. 使用 spi_send() 將要傳送的資料寫入 SPI_DR 暫存器中。此函式會先等待目前的傳輸已經結束後(SPI_SR_TXE flag)才將資料寫入資料暫存器。
  3. 讀取 SPI_SR_TXE(傳送緩衝器爲空) 與 SPI_SP_BSY(忙碌) flag,以等待 SPI 完成傳輸。
  4. 取消選擇 Slave device(CS 輸出 High)。

EXTI ISR

 1/**
 2 * @brief EXTI9~5 Interrupt service routine.
 3 */
 4void exti9_5_isr(void)
 5{
 6  exti_reset_request(EXTI_SPI_RQ);
 7
 8  spi_select();
 9  spi_send(SPI1, 0x00);               /* Just for beget clock signal. */
10  while ((SPI_SR(SPI1) & SPI_SR_BSY)) /* Wait for 'Busy' flag to reset. */
11  { }
12  uint8_t indata = spi_read(SPI1);
13
14  while ((SPI_SR(SPI1) & SPI_SR_BSY)) /* Wait for 'Busy' flag to reset. */
15  { }
16  spi_deselect();
17
18  usart_send_blocking(USART2, indata);
19}

當 RQ 請求腳被觸發(Low 觸發)時,代表 Slave device 想發起通訊,因此 Master device 要拉低 CS 腳以選擇 Slave device,並讀取 MISO 的資料。

要注意的是 SPI slave device 不會自己產生 SCK 時脈訊號,SCK 是由 Master device 產生的,而在這裡單純呼叫 spi_read() 也不會讓 Master device 產生 SCK 訊號,因此要呼叫 spi_send() 並傳送一個假資料(這裡爲 0x00)讓 SCK 產生。

多環境程式(F446RE + F103RB)

由於 STM32F1 的部分函式不同,所以 F103RB 沒辦法直接使用上面的 F446RE 的程式。

由於這次程式較長,所以完整的程式請看 GitHub repo

成果

由於下一篇才會寫 SPI slave,因此這次就先只以邏輯分析儀查看 SPI 的輸出。

訊號波形由上而下是 CS(D4)、SCK(D7)、MOSI(D5)與 MISO(D6)。

我傳送的資料是 0xA7,也就是 1010 0111b,以 SCK 的負緣對照 MOSI 訊號也是正確的。

小結

SPI 是許多感測器及模組在使用的通訊介面,會使用 SPI 才能使用這些外部元件,因此 SPI 也是很重要的功能。這次介紹了最基本的 SPI 用法,應該已經足夠應付基本的使用了。

參考資料

本文的程式也有放在 GitHub 上。
本文同步發表於 iT 邦幫忙-2022 iThome 鐵人賽



留言可能不會立即顯示。若過了幾天仍未出現,請 Email 聯繫:)

comments powered by Disqus