使用Platformio平台的libopencm3开发框架来开发STM32G0,以下使用软件模拟I2C总线时序,并用它来读取GXHT30温湿度数据。

1 新建项目

  • 建立gxht30项目

在PIO的Home页面新建项目,项目名称gxht30,选择开发板为 MonkeyPi_STM32_G070RB,开发框架选择libopencm3;

  • 项目建立完成后在src目录下新建main.c主程序文件;
  • 修改下载和调试方式,这里开发板使用的是DAPLink仿真器,因此修改platformio.ini文件如下:
1
2
upload_protocol = cmsis-dap
debug_tool = cmsis-dap

2 I2C软件模拟

2.1 文件结构

在lib目录新建 sw_i2c 文件夹,并新建如下文件:

2.2 sw_i2c_port.h 与底层IO读写相关
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* @file sw_i2c_port.h
* @author MakerInChina (makerinchina.cn)
* @brief
* @version 0.01
* @date 2022-09-25
*
* @copyright Copyright (c) 2022
*
*/

#ifndef _SW_I2C_PORT_HEAD_H_
#define _SW_I2C_PORT_HEAD_H_

#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>

#define SW_I2C_SCL_CLOCK RCC_GPIOB
#define SW_I2C_SCL_PORT GPIOB
#define SW_I2C_SCL_PIN GPIO13

#define SW_I2C_SDA_CLOCK RCC_GPIOB
#define SW_I2C_SDA_PORT GPIOB
#define SW_I2C_SDA_PIN GPIO14

#define sw_i2c_scl_high() gpio_set(SW_I2C_SCL_PORT, SW_I2C_SCL_PIN)
#define sw_i2c_scl_low() gpio_clear(SW_I2C_SCL_PORT, SW_I2C_SCL_PIN)
#define sw_i2c_sda_high() gpio_set(SW_I2C_SDA_PORT, SW_I2C_SDA_PIN)
#define sw_i2c_sda_low() gpio_clear(SW_I2C_SDA_PORT, SW_I2C_SDA_PIN)

#define sw_i2c_sda_input() gpio_mode_setup(SW_I2C_SDA_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, SW_I2C_SDA_PIN)
#define sw_i2c_sda_output() gpio_mode_setup(SW_I2C_SDA_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, SW_I2C_SDA_PIN)

// #define sw_i2c_delay() delay_us(5)
#define sw_i2c_delay() do{ \
for (int i=0; i<58; i++) { \
__asm__ volatile ("nop"); \
} \
}while(0)
static bool sw_i2c_sda_get(void)
{
return (gpio_get(SW_I2C_SDA_PORT, SW_I2C_SDA_PIN) != 0) ? true:false;
}

static void sw_i2c_port_init()
{
/* 打开GPIO时钟 */
rcc_periph_clock_enable(SW_I2C_SCL_CLOCK);
rcc_periph_clock_enable(SW_I2C_SDA_CLOCK);

/* 禁用默认上拉,使SCL, SDA保持高阻状态, 设置为 OD 模式 */
gpio_mode_setup(SW_I2C_SCL_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, SW_I2C_SCL_PIN);
gpio_mode_setup(SW_I2C_SDA_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, SW_I2C_SDA_PIN);
gpio_set_output_options(SW_I2C_SCL_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_25MHZ, SW_I2C_SCL_PIN);
gpio_set_output_options(SW_I2C_SDA_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_25MHZ, SW_I2C_SDA_PIN);

/* 空闲: 拉高SCL和SDA */
gpio_set(SW_I2C_SCL_PORT, SW_I2C_SCL_PIN);
gpio_set(SW_I2C_SDA_PORT, SW_I2C_SDA_PIN);
}

#endif //!_SW_I2C_PORT_HEAD_H_

i2c时序中的延时这里使用软件延时,模拟的是 100KHz的频率;

2.3 sw_i2c_private.h 实现i2c的基本时序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/**
* @file sw_i2c_private.h
* @author MakerInChina (makerinchina.cn)
* @brief
* @version 0.01
* @date 2022-09-25
*
* @copyright Copyright (c) 2022
*
*/

#ifndef _SW_I2C_PRIVATE_HEAD_H_
#define _SW_I2C_PRIVATE_HEAD_H_

#include "sw_i2c_port.h"

static void i2c_start(void);
static void i2c_stop(void);
static bool i2c_wait_ack(void);
static void i2c_send_ack(void);
static void i2c_send_nack(void);
static void i2c_send_byte(uint8_t data);
static uint8_t i2c_recv_byte(bool ack);

/**
* @brief I2C总线启动信号
*/
static void i2c_start(void)
{
/* 当SCL高电平时,SDA出现一个下跳沿表示I2C总线启动信号 */
sw_i2c_sda_high();
sw_i2c_scl_high();
sw_i2c_delay();
sw_i2c_sda_low();
sw_i2c_delay();
sw_i2c_scl_low();
sw_i2c_delay();
}

/**
* @brief I2C总线停止信号
*/
static void i2c_stop(void)
{
/* 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 */
sw_i2c_sda_low();
sw_i2c_delay();
sw_i2c_scl_high();
sw_i2c_delay();
sw_i2c_sda_high();
}

/**
* @brief 向I2C总线设备发送1个字节
* @param data 等待发送的字节
*/
static void i2c_send_byte(uint8_t data)
{
uint8_t i;

/* 先发送字节的高位bit7 */
for (i = 0; i < 8; i++) {
sw_i2c_delay();
sw_i2c_scl_low();

if (data & 0x80) {
sw_i2c_sda_high();
} else {
sw_i2c_sda_low();
}

sw_i2c_delay();
sw_i2c_scl_high();

data <<= 1; /* 左移一个bit */
}
}

/**
* @brief 产生一个时钟,并读取器件的ACK应答信号
* @return 返回true表示正确应答,false表示无器件响应
*/
static bool i2c_wait_ack(void)
{
bool res;

sw_i2c_delay();
sw_i2c_scl_low();

sw_i2c_sda_input();

sw_i2c_delay();
sw_i2c_scl_high(); /* 驱动SCL = 1, 此时器件会返回ACK应答 */
sw_i2c_delay();
if (sw_i2c_sda_get() == false) { /* 读取SDA口线状态 */
res = true;
} else {
res = false;
}
sw_i2c_scl_low();
sw_i2c_sda_high(); /* 释放SDA总线 */
sw_i2c_sda_output();
sw_i2c_delay();

return res;
}

/**
* @brief 产生一个ACK信号
*/
static void i2c_send_ack(void)
{
sw_i2c_sda_low(); /* CPU驱动SDA = 0 */
sw_i2c_delay();
sw_i2c_scl_high(); /* CPU产生1个时钟 */
sw_i2c_delay();
sw_i2c_scl_low();
sw_i2c_delay();
sw_i2c_sda_high(); /* CPU释放SDA总线 */
}

/**
* @brief CPU产生1个NACK信号
*/
static void i2c_send_nack(void)
{
sw_i2c_sda_high(); /* CPU驱动SDA = 1 */
sw_i2c_delay();
sw_i2c_scl_high(); /* CPU产生1个时钟 */
sw_i2c_delay();
sw_i2c_scl_low();
sw_i2c_delay();
}

/**
* @brief CPU从I2C总线设备读取8bit数据
* 读1个字节,ack=1时,发送ACK,ack=0,发送nACK
* @return
*/
static uint8_t i2c_recv_byte(bool ack)
{
uint8_t i;
uint8_t value;

/* 读到第1个bit为数据的bit7 */
value = 0;
for (i = 0; i < 8; i++) {
value <<= 1;
sw_i2c_scl_high();
sw_i2c_delay();
if (sw_i2c_sda_get()==true) {
value++;
}
sw_i2c_scl_low();
sw_i2c_delay();
}

if (ack) {
i2c_send_ack(); //发送ACK
} else {
i2c_send_nack();//发送nACK
}

return value;
}

#endif //!_SW_I2C_PRIVATE_HEAD_H_
2.4 sw_i2c 读写实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/**
* @file sw_i2c.c
* @author MakerInChina (makerinchina.cn)
* @brief
* @version 0.01
* @date 2022-09-25
*
* @copyright Copyright (c) 2022
*
*/

#include "sw_i2c.h"
#include "sw_i2c_port.h"
#include "sw_i2c_private.h"

void sw_i2c_init()
{
sw_i2c_port_init();
}

/* Function to setup and execute I2C transfer request */
bool sw_i2c_transfer(uint8_t dev_addr, uint8_t *tx_buffer,uint16_t tx_size,uint8_t *rx_buffer,uint16_t rx_size)
{
uint16_t i;

if (tx_size > 0) {
/* start */
i2c_start();
/* address + write */
i2c_send_byte(dev_addr<<1);
if (i2c_wait_ack() == false) {
goto error_device_nack;
}
/* write data */
for (i=0; i<tx_size; i++) {
i2c_send_byte(tx_buffer[i]);
if (i2c_wait_ack() == false) {
goto error_device_nack;
}
}
}
if (rx_size > 0) {
/* start */
i2c_start();
/* address + read */
i2c_send_byte(dev_addr<<1 | 1);
if (i2c_wait_ack() == false) {
goto error_device_nack;
}
/* read data */
for (i=0; i<rx_size; i++) {
rx_buffer[i] = i2c_recv_byte(i+1<rx_size);
}
}
i2c_stop();
return true;

error_device_nack:
i2c_stop();
return false;
}

// Scan the I2C bus between addresses from_addr and to_addr.
// On each address, call the callback function with the address and result.
// If result==0, address was found, otherwise, address wasn't found
// (can use result to potentially get other status on the I2C bus, see twi.c)
// Assumes Wire.begin() has already been called
void scan_i2c_bus(uint8_t from_addr, uint8_t to_addr, void(*callback)(uint8_t address, uint8_t result))
{
bool rc;
uint8_t dev_addr_7bit = 0;

for( uint8_t addr = from_addr; addr <= to_addr; addr++) {

/* start */
i2c_start();

/* address + write */
i2c_send_byte(addr);

if (i2c_wait_ack() == false) {
rc = false;
}else{
rc = true;
}

i2c_stop();

dev_addr_7bit = addr>>1;

callback(dev_addr_7bit, rc);

//dealy for sometime, 5 clk
for(char i=0; i<5; i++){
sw_i2c_delay();
}

//ignore add+1, read

addr++;
if(addr > to_addr){
break;
}

}
}
  • 实现了数据传输 transfer 接口,包含了发送和接收;
  • 实现总线设备扫描功能,可以用于辅助调试;

3 GXHT30使用I2C

3.1 扫描设备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void scan_i2c_cb( uint8_t addr, uint8_t result )
{

if(result == 1){
printf(" scan addr[7bit]: 0x%x found!\r\n",addr);
}else{
// printf("scan addr: %x not found\r\n",addr); //not found
}

}

int main(void)
{
...

printf("init i2c bus\r\n");

sw_i2c_init();

printf("scan device on i2c bus...\r\n");

scan_i2c_bus(0x02,0xfe, scan_i2c_cb);

...
}
3.2 读取温湿度数据

根据GXHT30芯片手册实现,这里为单次读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void gxht30_sample(float *temp, float *humi)
{
uint8_t rd_buff[6] = {0};
uint8_t cmd[2] = {0x2c, 0x06};

uint8_t dev_addr = 0x44;

//send read cmd
sw_i2c_transfer(dev_addr, cmd, 2, 0, 0);

delay_ms(10);

//receive data
sw_i2c_transfer(dev_addr, 0,0, rd_buff, 6);

uint16_t temp_int = (uint16_t)((rd_buff[0] << 8)|(rd_buff[1]));
uint16_t humi_int = (uint16_t)((rd_buff[3] << 8)|(rd_buff[4]));

*temp = -45 + (float)(175*temp_int/65535.0000);
*humi = 100 * (float)(humi_int /65535.0000);
}

发送命令的波形也和预期一致;

  • 发送读取命令 0x2c 0x06 的波形:
  • 接收数据的波形,温度+CRC+湿度+CRC:

4 烧写测试

4.1 连线

将开发板和温湿度模块的I2C引脚连接:

4.2 测试结果

可以看到读取到0x44的设备地址,即温湿度模块的I2C地址,温湿度读取正确: