前言
在上一篇中,我簡單介紹了 SPI 的用法,而除了 SPI 外還有另一種非常常見的通訊協定——I²C(以下稱 I2C)。
I2C 和 SPI 一樣是主從式架構,I2C 的主要特色就是無論有多少 Slave device 都只需要兩條線就可以完成通訊。
在這一篇文章中,我不會詳細介紹 I2C 本身,但建議還是要對它有基本的瞭解比較好,在此推薦「I2C bus 簡介 (Inter-Integrated Circuit Bus) @ 傑克! 真是太神奇了!」及「【Day21】I2C的介紹 - iT 邦幫忙」這兩篇文章。
24C256 是一個擁有 I2C 介面的 EEPROM,這次將示範如何使用 STM32 來透過 I2C 對其進行資料的讀寫,且可以用 USART 進行操作。
正文
首先一樣以 Nucleo-F446RE 做示範。
首先建立一個 PIO 的專案,選擇 Framework 爲「libopencm3」,並在 src/
資料夾中新增並開啓 main.c
與 main.h
。
完整程式
1/**
2 * @file main.c
3 * @brief I2C EEPROM (24C256) example for STM32 Nucleo-F446RE.
4 */
5
6#include "main.h"
7
8int main(void)
9{
10 rcc_setup();
11 i2c_setup();
12 usart_setup();
13
14 while (1)
15 { }
16 return 0;
17}
18
19static void rcc_setup(void)
20{
21 rcc_clock_setup_pll(&rcc_hse_8mhz_3v3[RCC_CLOCK_3V3_84MHZ]);
22
23 rcc_periph_clock_enable(RCC_I2C_GPIO);
24 rcc_periph_clock_enable(RCC_I2C1);
25 rcc_periph_clock_enable(RCC_USART_TXRX_GPIO);
26 rcc_periph_clock_enable(RCC_USART2);
27}
28
29static void i2c_setup(void)
30{
31 /* Set SCL & SDA pin to open-drain alternate function. */
32 gpio_mode_setup(GPIO_I2C_PORT,
33 GPIO_MODE_AF,
34 GPIO_PUPD_NONE,
35 GPIO_I2C_SCL_PIN | GPIO_I2C_SDA_PIN);
36
37 gpio_set_output_options(GPIO_I2C_PORT,
38 GPIO_OTYPE_OD,
39 GPIO_OSPEED_50MHZ,
40 GPIO_I2C_SCL_PIN | GPIO_I2C_SDA_PIN);
41
42 gpio_set_af(GPIO_I2C_PORT,
43 GPIO_I2C_AF,
44 GPIO_I2C_SCL_PIN | GPIO_I2C_SDA_PIN);
45
46 uint32_t i2c = I2C1;
47
48 i2c_peripheral_disable(i2c);
49 i2c_reset(i2c);
50
51 i2c_set_speed(i2c,
52 i2c_speed_fm_400k, /* 400 kHz Fast mode. */
53 rcc_apb1_frequency / 1e6); /* I2C clock in MHz. */
54
55 i2c_peripheral_enable(i2c);
56}
57
58static void usart_setup(void)
59{
60 /* Set USART-Tx & Rx pin to alternate function. */
61 gpio_mode_setup(GPIO_USART_TXRX_PORT,
62 GPIO_MODE_AF,
63 GPIO_PUPD_NONE,
64 GPIO_USART_TX_PIN | GPIO_USART_RX_PIN);
65
66 gpio_set_af(GPIO_USART_TXRX_PORT,
67 GPIO_USART_AF,
68 GPIO_USART_TX_PIN | GPIO_USART_RX_PIN);
69
70 /* Setup interrupt. */
71 nvic_enable_irq(NVIC_USART2_IRQ);
72 usart_enable_rx_interrupt(USART2);
73
74 /* Config USART params. */
75 usart_set_baudrate(USART2, USART_BAUDRATE);
76 usart_set_databits(USART2, 8);
77 usart_set_stopbits(USART2, USART_STOPBITS_1);
78 usart_set_parity(USART2, USART_PARITY_NONE);
79 usart_set_flow_control(USART2, USART_FLOWCONTROL_NONE);
80 usart_set_mode(USART2, USART_MODE_TX_RX);
81
82 usart_enable(USART2);
83}
84
85static void delay(uint32_t value)
86{
87 for (uint32_t i = 0; i < value; i++)
88 {
89 __asm__("nop"); /* Do nothing. */
90 }
91}
92
93/**
94 * @brief USART2 Interrupt service routine.
95 */
96void usart2_isr(void)
97{
98 usart_disable_rx_interrupt(USART2);
99
100 uint8_t cmd = usart_recv(USART2);
101 if (cmd == 0x00) /* Write command. */
102 {
103 uint8_t i2c_rx_data[1];
104 uint8_t i2c_tx_data[3];
105 i2c_tx_data[0] = usart_recv_blocking(USART2); /* Address 1. */
106 i2c_tx_data[1] = usart_recv_blocking(USART2); /* Address 2. */
107 i2c_tx_data[2] = usart_recv_blocking(USART2); /* Data. */
108
109 i2c_transfer7(I2C1,
110 I2C_SLAVE_ADDRESS,
111 i2c_tx_data, /* Tx data array. */
112 3, /* Tx data length. */
113 i2c_rx_data, /* Rx data array. */
114 0); /* Rx data lenght. */
115
116 usart_send_blocking(USART2, 0xF0); /* Write done ACK. */
117 }
118 else if (cmd == 0x01) /* Read command. */
119 {
120 uint8_t i2c_rx_data[1];
121 uint8_t i2c_tx_data[2];
122 i2c_tx_data[0] = usart_recv_blocking(USART2); /* Address 1. */
123 i2c_tx_data[1] = usart_recv_blocking(USART2); /* Address 2. */
124
125 i2c_transfer7(I2C1,
126 I2C_SLAVE_ADDRESS,
127 i2c_tx_data, /* Tx data array. */
128 2, /* Tx data length. */
129 i2c_rx_data, /* Rx data array. */
130 1); /* Rx data lenght. */
131
132 usart_send_blocking(USART2, i2c_rx_data[0]);
133 }
134 else /* Unknown command. */
135 {
136 usart_send_blocking(USART2, 0xFF);
137 }
138
139 /* Clear 'Read data register not empty' flag. */
140 USART_SR(USART2) &= ~USART_SR_RXNE;
141 usart_enable_rx_interrupt(USART2);
142}
1/** @file main.h */
2
3#ifndef MAIN_H
4#define MAIN_H
5
6#include <libopencm3/stm32/rcc.h>
7#include <libopencm3/stm32/gpio.h>
8#include <libopencm3/stm32/i2c.h>
9#include <libopencm3/stm32/usart.h>
10#include <libopencm3/cm3/nvic.h>
11
12#define I2C_SLAVE_ADDRESS ((uint8_t)0x50)
13#define USART_BAUDRATE (9600)
14
15#define RCC_I2C_GPIO (RCC_GPIOB)
16#define GPIO_I2C_PORT (GPIOB)
17#define GPIO_I2C_SCL_PIN (GPIO8) /* D15. */
18#define GPIO_I2C_SDA_PIN (GPIO9) /* D14. */
19#define GPIO_I2C_AF (GPIO_AF4) /* Ref: Table-11 in DS10693. */
20
21#define RCC_USART_TXRX_GPIO (RCC_GPIOA)
22#define GPIO_USART_TXRX_PORT (GPIOA)
23#define GPIO_USART_TX_PIN (GPIO2) /* ST-Link (D1). */
24#define GPIO_USART_RX_PIN (GPIO3) /* ST-Link (D0). */
25#define GPIO_USART_AF (GPIO_AF7) /* Ref: Table-11 in DS10693. */
26
27static void rcc_setup(void);
28static void i2c_setup(void);
29static void delay(uint32_t value);
30static void usart_setup(void);
31
32#endif /* MAIN_H. */
分段說明
Include
1// main.h
2#include <libopencm3/stm32/rcc.h>
3#include <libopencm3/stm32/gpio.h>
4#include <libopencm3/stm32/i2c.h>
5#include <libopencm3/stm32/usart.h>
6#include <libopencm3/cm3/nvic.h>
除了基本的 rcc.h
和 gpio.h
及這次的 i2c.h
外,因爲我要使用 USART 和中斷功能,所以還會需要 usart.h
與 nvic.h
。
設定 I2C
1static void i2c_setup(void)
2{
3 /* Set SCL & SDA pin to open-drain alternate function. */
4 gpio_mode_setup(GPIO_I2C_PORT,
5 GPIO_MODE_AF,
6 GPIO_PUPD_NONE,
7 GPIO_I2C_SCL_PIN | GPIO_I2C_SDA_PIN);
8
9 gpio_set_output_options(GPIO_I2C_PORT,
10 GPIO_OTYPE_OD,
11 GPIO_OSPEED_50MHZ,
12 GPIO_I2C_SCL_PIN | GPIO_I2C_SDA_PIN);
13
14 gpio_set_af(GPIO_I2C_PORT,
15 GPIO_I2C_AF,
16 GPIO_I2C_SCL_PIN | GPIO_I2C_SDA_PIN);
17
18 uint32_t i2c = I2C1;
19
20 i2c_peripheral_disable(i2c);
21 i2c_reset(i2c);
22
23 i2c_set_speed(i2c,
24 i2c_speed_fm_400k, /* 400 kHz Fast mode. */
25 rcc_apb1_frequency / 1e6); /* I2C clock in MHz. */
26
27 i2c_peripheral_enable(i2c);
28}
首先一樣先設定好 I2C 要使用的 SCL 與 SDA 接腳,將其設爲 Open-Drain 的 AF 功能。
再來要設定 I2C 本身。不同於 SPI 規定比較寬鬆(或說自由),I2C 本身的通訊規範基本上都定義好了,所以我們需要調整(或說可以調整)的設定就很少。這裡我們只需要設定要使用的 I2C 速度即可。
24C256 支援的 I2C 速度模式有:
- Standard mode: 100 kbps
- Fast mode: 400 kbps
- Fast mode Plus: 1Mbps
這裡我選擇使用「Fast mode」。以 i2c_set_speed()
函式進行設定,此函式的第二個引數 i2c_speed_fm_400k
就代表要使用「Fast mode」,而第三個引數要給的是 I2C 的時脈,對於 F446RE 或大多數的 STM32,這個速度等同 APB1。
USART ISQ
1/**
2 * @brief USART2 Interrupt service routine.
3 */
4void usart2_isr(void)
5{
6 usart_disable_rx_interrupt(USART2);
7
8 uint8_t cmd = usart_recv(USART2);
9 if (cmd == 0x00) /* Write command. */
10 {
11 uint8_t i2c_rx_data[1];
12 uint8_t i2c_tx_data[3];
13 i2c_tx_data[0] = usart_recv_blocking(USART2); /* Address 1. */
14 i2c_tx_data[1] = usart_recv_blocking(USART2); /* Address 2. */
15 i2c_tx_data[2] = usart_recv_blocking(USART2); /* Data. */
16
17 i2c_transfer7(I2C1,
18 I2C_SLAVE_ADDRESS,
19 i2c_tx_data, /* Tx data array. */
20 3, /* Tx data length. */
21 i2c_rx_data, /* Rx data array. */
22 0); /* Rx data lenght. */
23
24 usart_send_blocking(USART2, 0xF0); /* Write done ACK. */
25 }
26 else if (cmd == 0x01) /* Read command. */
27 {
28 uint8_t i2c_rx_data[1];
29 uint8_t i2c_tx_data[2];
30 i2c_tx_data[0] = usart_recv_blocking(USART2); /* Address 1. */
31 i2c_tx_data[1] = usart_recv_blocking(USART2); /* Address 2. */
32
33 i2c_transfer7(I2C1,
34 I2C_SLAVE_ADDRESS,
35 i2c_tx_data, /* Tx data array. */
36 2, /* Tx data length. */
37 i2c_rx_data, /* Rx data array. */
38 1); /* Rx data lenght. */
39
40 usart_send_blocking(USART2, i2c_rx_data[0]);
41 }
42 else /* Unknown command. */
43 {
44 usart_send_blocking(USART2, 0xFF);
45 }
46
47 /* Clear 'Read data register not empty' flag. */
48 USART_SR(USART2) &= ~USART_SR_RXNE;
49 usart_enable_rx_interrupt(USART2);
50}
這是 USART 的 ISQ。
我自己定義了一個簡單的 USART 指令格式:<RW> <Address_1> <Address_2> <Data>
若要在 24C256 的 0x0102
位置寫入資料 0xAB
,就是用 USART 傳送:0x00 0x01 0x02 0xAB
,完成後會收到一個 0xF0
作爲 ACK 確認。同理,要在 0x0FCD
寫入 0x40
拿就是要傳送 0x00 0x0F 0xCD 0x40
。
要讀取 0x0102
位置的資料的話,那就是用 USART 傳送:0x01 0x01 0x02
,然後 STM32 就會回傳該位置的資料。
24C256 的定址範圍爲
0x0000
~0x7FFF
共 32768 個位置,每個位置皆爲一個 Byte。
當 USART 接收到一筆資料時,會先判斷這是要進行寫(0x00
)還是讀(0x01
)。然後再使用 I2C 傳送資料。
i2c_transfer7()
用來進行 I2C 的傳輸,讀和寫都靠它。其參數意義依序爲:
- 使用的 I2C。這裡是用
I2C1
。 - 要溝通的 Slave device I2C 7-bit 位置。24C256 的預設位置爲
0x50
。 - 傳送資料陣列,即要傳送的位元組陣列。
- 傳送資料長度,要傳送幾個 Byte。填
0
代表不進行傳送。 - 接收資料陣列,接收到的資料會存進來。
- 接收資料長度,要接收幾個 Byte。填
0
代表不進行接收。
24C256 基本的讀寫操作也是很簡單。要寫的話就是依序傳送「位置-高
、位置-低
、資料
」這 3 個位元組即可。要讀的話就是依序傳送「位置-高
、位置-低
」這 2 個位元組,然後就可以讀取 該位置的資料位元組。
因此寫入的程式爲:
1uint8_t i2c_rx_data[1];
2uint8_t i2c_tx_data[3];
3i2c_tx_data[0] = usart_recv_blocking(USART2); /* Address 1. */
4i2c_tx_data[1] = usart_recv_blocking(USART2); /* Address 2. */
5i2c_tx_data[2] = usart_recv_blocking(USART2); /* Data. */
6
7i2c_transfer7(I2C1,
8 I2C_SLAVE_ADDRESS,
9 i2c_tx_data, /* Tx data array. */
10 3, /* Tx data length. */
11 i2c_rx_data, /* Rx data array. */
12 0); /* Rx data lenght. */
而讀取的程式爲:
1uint8_t i2c_rx_data[1];
2uint8_t i2c_tx_data[2];
3i2c_tx_data[0] = usart_recv_blocking(USART2); /* Address 1. */
4i2c_tx_data[1] = usart_recv_blocking(USART2); /* Address 2. */
5
6i2c_transfer7(I2C1,
7 I2C_SLAVE_ADDRESS,
8 i2c_tx_data, /* Tx data array. */
9 2, /* Tx data length. */
10 i2c_rx_data, /* Rx data array. */
11 1); /* Rx data lenght. */
12
13usart_send_blocking(USART2, i2c_rx_data[0]);
多環境程式(F446RE + F103RB)
由於 STM32F1 的部分函式不同,所以 F103RB 沒辦法直接使用上面的 F446RE 的程式。
由於本例的差異比較大,爲了不佔版面這裡就不列出的,完整的程式請看 GitHub repo。
特別要主要的是,F103RB 要使用 PB8 和 PB9 作爲 I2C 的 SCL 及 SDA 腳時,要啓用「Remap」。詳細請參考 DS5319 的 Table 5。
1static void i2c_setup(void)
2{
3 /* Set SCL & SDA pin to open-drain alternate function. */
4#if defined(STM32F1)
5 gpio_set_mode(GPIO_I2C_PORT,
6 GPIO_MODE_OUTPUT_50_MHZ,
7 GPIO_CNF_OUTPUT_ALTFN_OPENDRAIN,
8 GPIO_I2C_SCL_PIN | GPIO_I2C_SDA_PIN);
9
10 /*
11 * Alternate function remap is required for
12 * using I2C1_SCL & SDA on PB8 & PB9.
13 * Refer to Table-5 in DS5319.
14 */
15 gpio_primary_remap(AFIO_MAPR_SWJ_CFG_FULL_SWJ,
16 AFIO_MAPR_I2C1_REMAP);
17#else
18 gpio_mode_setup(GPIO_I2C_PORT,
19 GPIO_MODE_AF,
20 GPIO_PUPD_NONE,
21 GPIO_I2C_SCL_PIN | GPIO_I2C_SDA_PIN);
22
23 gpio_set_output_options(GPIO_I2C_PORT,
24 GPIO_OTYPE_OD,
25 GPIO_OSPEED_50MHZ,
26 GPIO_I2C_SCL_PIN | GPIO_I2C_SDA_PIN);
27
28 gpio_set_af(GPIO_I2C_PORT,
29 GPIO_I2C_AF,
30 GPIO_I2C_SCL_PIN | GPIO_I2C_SDA_PIN);
31#endif
32 uint32_t i2c = I2C1;
33
34 i2c_peripheral_disable(i2c);
35 i2c_reset(i2c);
36
37 i2c_set_speed(i2c,
38 i2c_speed_fm_400k, /* 400 kHz Fast mode. */
39 rcc_apb1_frequency / 1e6); /* I2C clock in MHz. */
40
41 i2c_peripheral_enable(i2c);
42}
成果
我首先將 0xAB
寫入 0x0000
(00 00 00 AB
),再寫入 0x39
到 0x0001
(00 00 01 39
)。
然後讀取 0x0000
(01 00 00
)得到回傳的 0xAB
,再讀取 0x0001
(01 00 01
)得到 0x39
。
最後再次寫入 0xCD
到 0x0000
(00 00 00 CD
),再讀取它(01 00 00
)得到 0xCD
。
小結
這次介紹了 I2C 的程式寫法。SPI 與 I2C 是各種電路模組或 IC 會使用的通訊協定,只要會使用 SPI 與 I2C,那基本上常見的模組都可以使用了,因此 I2C 是一個很重要的功能,還好 STM32 本身的硬體及 LibOpenCM3 都把那些複雜的設定做好了,因此要使用 I2C 相當容易。
參考資料
- libopencm3/libopencm3-examples
- platformio/platform-ststm32
- STM32F446RE datasheet (DS10693)
- STM32F446xx reference manual (RM0390)
- STM32F103RB datasheet (DS5319)
- STM32 Nucleo-64 board user manual (UM1724)
本文的程式也有放在 GitHub 上。
本文同步發表於 iT 邦幫忙-2022 iThome 鐵人賽。
留言可能不會立即顯示。若過了幾天仍未出現,請 Email 聯繫:)
comments powered by Disqus