【实测分享】STM32驱动BH1750:软件I2C通信、光学窗口补偿(附开源代码)

摘要

BH1750FVI 是 ROHM 公司推出的一款数字环境光传感器,通过 I²C 接口直接输出 16 位光照强度数值(单位:lux)。与传统光敏电阻或模拟传感器不同,它内置 ADC 和逻辑电路,无需外接元件即可实现近似人眼的光谱响应,测量范围覆盖 1~65535 lux,广泛应用于手机背光调节、智能家居照明控制及户外亮度监测等场景。

本文将首先介绍如何使用 BH1750 。

开源链接

下面是我的代码在 gitee 和 github 的开源链接。

参数
github主页 https://github.com/snqx-lqh
github项目地址 https://github.com/snqx-lqh/DeviceDrivers
gitee项目地址 https://gitee.com/snqx-lqh/DeviceDrivers
作者 VX Qinghua_Li7

在我的这个项目中,找到 Example 目录下的 02_BH1750 。需要注意的是,复制工程记得还要复制源码。不要光把工程复制出去了。

BH1750 工作流程

BH1750 的工作流程如下图。图中有两种线。一种实线,一种虚线。实线代表上一步执行完成后会自动跳转到下一步,虚线代表需要使用 I2C 发送指令才会跳转到下一步。
image

  1. 上电后。芯片处于 Power Down(断电) 模式。这时候芯片并未进入工作模式。
  2. 下一步我们需要发送指令让芯片处于 Power On 模式(工作模式)。
  3. 接下来发送 Measurement Command (测量指令)。让芯片进入 One Time Measurement(单次测量状态)或者 Continuous Measurement(连续测量状态)
  4. 读取测量结果并且转换成光照值。
  5. 单次测量模式,在测量完成后,又会自动回到 Power Down (断电)的模式。需要重新开启工作模式。但是连续的模式不会。

I2C流程

BH1750的I2C处理,比较简易。他没有寄存器这个概念,就是发送地址后直接发指令或者读数据。该芯片支持两种地址。

ADDRGND: 0100011(0x23)

ADDRVCC: 1011100(0x5c)

数据发送

数据发送(指令发送)I2C处理:

image

可以看到,流程就是

发送起始信号->发送(地址7位+1位读写位(0代表写))->等待ACK->发送数据(这里一般就是填相关指令)->等待ACK->发送停止信号。

数据接收

数据接收I2C处理:

image

可以看到,流程就是

发送起始信号->发送(地址7位+1位读写位(1代表读))->等待ACK->读高8位->发送ACK响应->读低8位->发送NACK响应->发送停止信号。

指令功能

该芯片各个操作的具体指令如下,后面用到再介绍。

指令 操作码 说明
断电 0000_0000 无活动状态。
通电 0000_0001 等待测量命令。
复位 0000_0111 复位数据寄存器值。复位命令在断电模式下不接受。
连续 H-Resolution 模式 0001_0000 以 1 lx 分辨率开始测量。测量时间通常为 120 ms。
连续 H-Resolution Mode2 0001_0001 以 0.5 lx 分辨率开始测量。测量时间通常为 120 ms。
连续 L-Resolution 模式 0001_0011 以 4 lx 分辨率开始测量。测量时间通常为 16 ms。
单次 H-Resolution 模式 0010_0000 以 1 lx 分辨率开始测量。测量时间通常为 120 ms。测量后自动设置为断电模式。
单次 H-Resolution Mode2 0010_0001 以 0.5 lx 分辨率开始测量。测量时间通常为 120 ms。测量后自动设置为断电模式。
单次 L-Resolution 模式 0010_0011 以 4 lx 分辨率开始测量。测量时间通常为 16 ms。测量后自动设置为断电模式。
更改测量时间(高位) 01000_MT[7,6,5] 更改测量时间。※请参阅"针对光学窗口影响调整测量结果"。
更改测量时间(低位) 011_MT[4,3,2,1,0] 更改测量时间。※请参阅"针对光学窗口影响调整测量结果"。

基础代码设计

首先,使用的前提是我们已经有了 I2C 传输数据的函数,这部分不做详细说明。我直接放 API。具体的代码我再结尾贴上,也可以去我的开源链接中查看。

uint8_t soft_i2c_write(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,const uint8_t *buf,uint8_t len);
uint8_t soft_i2c_read(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,uint8_t *buf,uint8_t len);

在.h文件中做基础定义:

#ifndef _BH_1750_H
#define _BH_1750_H

#include <stdint.h>

#include <stdint.h>

// I2C 地址(根据 ADDR 引脚电平)
#define BH1750_ADDR_L   0x23  
#define BH1750_WRITE_ADDR_L  0x23   
#define BH1750_WRITE_ADDR_H  0x5C  

// 命令定义(数据手册第5页)
#define BH1750_POWER_DOWN     0x00
#define BH1750_POWER_ON       0x01
#define BH1750_RESET          0x07  // 仅复位数据寄存器,不是阈值
#define BH1750_CONT_H_RES     0x10  // 连续 H 分辨率(1 lx)
#define BH1750_CONT_H_RES2    0x11  // 连续 H 分辨率模式2(0.5 lx)
#define BH1750_CONT_L_RES     0x13  // 连续 L 分辨率(4 lx,16ms)
#define BH1750_ONE_TIME_H     0x20  // 单次 H 分辨率(测量后自动断电)
#define BH1750_ONE_TIME_H2    0x21  // 单次 H 分辨率模式2 
#define BH1750_ONE_TIME_L     0x23  // 单次 L 分辨率

// MTreg 命令前缀(数据手册第11页)
#define BH1750_MT_H           0x40  // 01000_MT[7,6,5]
#define BH1750_MT_L           0x60  // 011_MT[4,3,2,1,0]

/* API */
uint8_t bh1750_init(uint8_t addr, uint8_t mode);
int bh1750_read_lux(uint8_t addr, uint8_t mode, float *lux);
int bh1750_read_lux_single(uint8_t addr,uint8_t mode, float *lux);
uint8_t bh1750_set_mtreg(uint8_t addr, uint8_t mt_val);
int bh1750_read_lux_ex(uint8_t addr, uint8_t mode, float *lux, uint8_t mtreg);
int bh1750_read_lux_single_ex(uint8_t addr, uint8_t mode, float *lux, uint8_t mtreg);

#endif

我把这个函数封装成了BH1750专门使用的函数。

/* 静态函数:发送命令 */
static uint8_t bh1750_send_cmd(uint8_t addr, uint8_t cmd)
{
    // 注意:确保 addr 是写地址(7位地址左移1位,最低位为0)
	// 我的 写函数中自动处理了 7位地址左移 所以这里直接把 7位地址传进来就行了。
    return soft_i2c_write(SOFT_I2C1, addr, &cmd, 1);
}

/* 静态函数:读取原始数据 */
static uint8_t bh1750_read_raw(uint8_t addr, uint8_t raw_data[2])
{
    return soft_i2c_read(SOFT_I2C1, addr, raw_data, 2);
}

然后是初始化。初始化我们包含启动芯片,设置测量模式。我的代码多加了一个复位寄存器的指令。理论上这个初始化最好只用于连续测量,单次测量也可以。但是建议单次测量单开函数,然后每次都走完整流程。不然会很容易忘了重新进入工作模式。

uint8_t bh1750_init(uint8_t addr, uint8_t mode)
{
    uint8_t ret;
    
    // 1. Power On(要先让设备进入工作模式,不然后续处理无效)
    ret = bh1750_send_cmd(addr, BH1750_POWER_ON);
    if (ret) return ret;
    
    // 2. Reset(清零数据寄存器,用于清除之前的测量结果)
    ret = bh1750_send_cmd(addr, BH1750_RESET);
    if (ret) return ret;
    
    // 3. 设置测量模式
    ret = bh1750_send_cmd(addr, mode);
    if (ret) return ret;
    
    // 4. 等待第一次测量完成(最大180ms)
    // 如果是单次模式,不需要在这里等,而是在 read 前等
    if ((mode == BH1750_CONT_H_RES) || (mode == BH1750_CONT_H_RES2)) {
        delay_ms(180);  // 等待首次测量完成
    } else if (mode == BH1750_CONT_L_RES) {
        delay_ms(24);   // L分辨率最大24ms
    }
    
    return 0;
}

连续工作模式,初始化后,就可以读取照度了。主要操作就是读出数据值后,做一个简单的解算。转换公式:lux = raw / 1.2。记得如果是0.5lx分辨率的那两个,计算的时候还要再除以2。

int bh1750_read_lux(uint8_t addr, uint8_t mode, float *lux)
{
    uint8_t ret;
    uint8_t raw_data[2];
    uint16_t raw;
    
    ret = bh1750_read_raw(addr, raw_data);
    if (ret) return -1;
    
    raw = (raw_data[0] << 8) | raw_data[1];
    
    // 使用 float 防止整数溢出
    *lux = (float)raw / 1.2f;
    // 0.5lx分辨率 需要除以2 
	if(mode == BH1750_CONT_H_RES2 || mode == BH1750_ONE_TIME_H2){
		*lux = *lux/2;
	}
    
    return 0;
}

关于单次测量,建议单开函数,设定完整流程,比较适合低功耗场景。

int bh1750_read_lux_single(uint8_t addr,uint8_t mode, float *lux)
{
    uint8_t ret;
    
    // 1. Power On
    ret = bh1750_send_cmd(addr, BH1750_POWER_ON);
    if (ret) return -1;
    
    // 2. 发送单次测量命令
    ret = bh1750_send_cmd(addr, mode);
    if (ret) return -1;
    
    // 3. 等待测量完成(最大180ms)
    delay_ms(180);
    
    // 4. 读取数据(传感器会自动进入 Power Down)
    return bh1750_read_lux(addr, mode, lux);
}

然后就可以做两个示例了,关于连续读取和单次读取。连续读取,打开一次工作模式就可以一直读取了,但是单次模式每次都要重新打开工作模式。

void exp_continuous_measure()
{
	bh1750_init(BH1750_WRITE_ADDR_L,BH1750_CONT_H_RES);
	while(1)
	{
		if (bh1750_read_lux(BH1750_WRITE_ADDR_L, BH1750_CONT_H_RES, &lux) == 0)
		{
			printf("continuous Light = %.2f lux\r\n", lux);
		}
		/* 高分辨率模式下,每次测量间隔至少 120ms */
        delay_ms(1000);  // 请替换为您的延时函数
	}
}	

void exp_single_measure()
{ 
	while(1)
	{ 
		if (bh1750_read_lux_single(BH1750_WRITE_ADDR_L, BH1750_ONE_TIME_H, &lux) == 0)
		{
			printf("single Light = %.2f lux\r\n", lux);
		}
		/* 高分辨率模式下,每次测量间隔至少 120ms */
        delay_ms(1000);  // 请替换为您的延时函数
	}
}	

可能的BUG

问题现象 可能原因 解决方案
I2C 无 ACK ADDR 引脚悬空/接错 接 GND(0x23)或 VCC(0x5C)
I2C 无 ACK I2C 写地址,7位地址和8位地址 看看API函数需要你传入的是7位地址还是把7位地址左移一位并带上读写位的8位地址
数据始终为 0 没等 180ms 首次转换 delay_ms(180) 后再读取

传感器灵敏度调整

这个功能一般用在传感器放在有遮挡的环境下。一般课设可能用不上。但是可以了解。

BH1750FVI 可以改变传感器灵敏度。通过使用此功能,可以消除光学窗口的影响(有/无光学窗口的差异)。通过更改测量时间进行调整。例如,当光学窗口的透过率为 50%(如果设置光学窗口,测量结果变为 0.5 倍)时,通过将传感器灵敏度从默认值更改为 2 倍,可以忽略光学窗口的影响。

通过更改 MTreg(测量时间寄存器)的值来偏移传感器灵敏度。如果目标传感器灵敏度为 2 倍,则 MTreg 值必须设置为 2 倍。当 MTreg 值从默认值更改为 2 倍时,测量时间也设置为 2 倍。

想象你把 BH1750 装在一个带玻璃罩的盒子里面测光线。玻璃会挡住一半的光(透过率 50%),传感器就以为环境很暗,读数只有实际值的一半。这时候你需要告诉传感器:"光线被玻璃挡住了一半,你要变得更敏感一些,把数值乘回去"。

更改测量灵敏度代码如下。可以看上面的指令表最后两项。可以发现,这两个指令,前面都会有一点固定值,然后设定值高3位放在一个指令中,低5位放在另一个指令中。这个窗口值的默认值是69。这个设置后,每次的测量时间延时也得修改。比如默认 69 延时 120ms 就能得到结果,那改到 69*2 就得需要 240ms。

uint8_t bh1750_set_mtreg(uint8_t addr, uint8_t mt_val)
{
    uint8_t ret;
    uint8_t high = BH1750_MT_H | ((mt_val >> 5) & 0x07);  // MT[7:5]
    uint8_t low  = BH1750_MT_L | (mt_val & 0x1F);         // MT[4:0]
    
    ret = bh1750_send_cmd(addr, high);
    if (ret) return ret;
    ret = bh1750_send_cmd(addr, low);
    return ret;
}

读取值得代码如下。主要是做了个测量比。

int bh1750_read_lux_ex(uint8_t addr, uint8_t mode, float *lux, uint8_t mtreg)
{
    uint8_t ret;
    uint8_t raw_data[2];
    uint16_t raw;
    
    ret = bh1750_read_raw(addr, raw_data);
    if (ret) return -1;
    
    raw = (raw_data[0] << 8) | raw_data[1];
    
    // 根据 MTreg 计算实际照度(数据手册第11页公式)
    // 默认 mtreg=69 时,系数为 1/1.2 ≈ 0.833
    // 使用 float 防止整数溢出
    *lux = (float)raw / 1.2f * (69.0f / (float)mtreg);
    
	if(mode == BH1750_CONT_H_RES2 || mode == BH1750_ONE_TIME_H2){
		*lux = *lux/2;
	}
	
    return 0;
}

int bh1750_read_lux_single_ex(uint8_t addr, uint8_t mode, float *lux, uint8_t mtreg)
{
    uint8_t ret;
    
    // 1. Power On
    ret = bh1750_send_cmd(addr, BH1750_POWER_ON);
    if (ret) return -1;
    
    // 2. 发送单次测量命令
    ret = bh1750_send_cmd(addr, mode);
    if (ret) return -1;
    
    // 3. 等待测量完成(最大180ms)
    delay_ms(180);
    
    // Step 4: 读取数据(传感器会自动进入 Power Down)
    return bh1750_read_lux_ex(addr, mode, lux, mtreg);
}

附录 soft_i2c实现

这里放上我的 i2c 实现。

bsp_soft_i2c.c

/**
  ******************************************************************************
  * @file    bsp_soft_i2c.c
  * @author  liqinghua <liqinghuaxx@163.com>
  * @version V1.0.0
  * @date    2026-01-29
  * @brief   软件I2C驱动模块
  *
  * @copyright (c) 2026 liqinghua
  * 
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
  * in the Software without restriction, including without limitation the rights
  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  * copies of the Software, and to permit persons to whom the Software is
  * furnished to do so, subject to the following conditions:

  * The above copyright notice and this permission notice shall be included in all
  * copies or substantial portions of the Software.

  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  * SOFTWARE.
  *
  * @details 
  *          本模块实现基于GPIO的软件I2C通信,支持多个I2C总线。
  *          支持的功能:
  *          - I2C初始化
  *          - I2C总线控制(起始、停止、应答等)
  *          - 单字节读写
  *          - 多字节读写
  *
  * @warning 引脚初始化时必须使用开漏输出模式(GPIO_Mode_Out_OD)
  */  
#include "bsp_soft_i2c.h"

/****************** user port area start ****************/
#include "bsp_delay.h"
#include "main.h"

/**
  * @brief  I2C延时函数(微秒级)
  * @note   延时时间用于控制I2C总线的时钟频率
  * @param  无
  * @retval 无
  */
void soft_i2c_delay_us()
{
	delay_us(4);
}

/**
  * @brief  I2C模块初始化
  * @note   注意:引脚初始化一定要是开漏模式(GPIO_Mode_Out_OD)
  *         SCL: PB6, SDA: PB7
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @retval 无
  */
void soft_i2c_init(SOFT_I2C_TypeDef soft_i2c)
{
	if(soft_i2c == SOFT_I2C1){
		GPIO_InitTypeDef GPIO_InitStructure;
		//开启端口时钟
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

		//配置SCL、SDA引脚
		GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
		GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
		GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //开漏输出
		GPIO_Init(GPIOB, &GPIO_InitStructure);
		
		GPIO_SetBits(GPIOB,GPIO_Pin_6);
		GPIO_SetBits(GPIOB,GPIO_Pin_7); //所有设备空闲,总线拉高电平
	}else if(soft_i2c == SOFT_I2C2){
	}
}

/**
  * @brief  设置SCL引脚电平
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @param  level: 目标电平
  *   @arg  0: 低电平
  *   @arg  1: 高电平
  * @retval 无
  */
void soft_i2c_set_scl_level(SOFT_I2C_TypeDef soft_i2c,uint8_t level)
{
	if(soft_i2c == SOFT_I2C1){
		if(level == 0) GPIO_ResetBits(GPIOB,GPIO_Pin_6);
		else GPIO_SetBits(GPIOB,GPIO_Pin_6);
	}else if(soft_i2c == SOFT_I2C2){}
}

/**
  * @brief  设置SDA引脚电平
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @param  level: 目标电平
  *   @arg  0: 低电平
  *   @arg  1: 高电平
  * @retval 无
  */
void soft_i2c_set_sda_level(SOFT_I2C_TypeDef soft_i2c,uint8_t level)
{
	if(soft_i2c == SOFT_I2C1){
		if(level == 0) GPIO_ResetBits(GPIOB,GPIO_Pin_7);
		else GPIO_SetBits(GPIOB,GPIO_Pin_7);
	}else if(soft_i2c == SOFT_I2C2){}
}

/**
  * @brief  读取SDA引脚电平
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @retval SDA引脚电平值 (0: 低电平, 1: 高电平)
  */
uint8_t soft_i2c_get_sda_level(SOFT_I2C_TypeDef soft_i2c)
{
	uint8_t level;
	if(soft_i2c == SOFT_I2C1){
		level = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_7);
	}else if(soft_i2c == SOFT_I2C2){}
	return level;
}
/****************** user port area end   ****************/

/**
  * @brief  I2C产生起始信号(START)
  * @details 起始条件:SCL高时,SDA从高变低
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @retval 无
  */
void soft_i2c_start(SOFT_I2C_TypeDef soft_i2c)
{
	soft_i2c_set_sda_level(soft_i2c,1);
	soft_i2c_set_scl_level(soft_i2c,1);
	soft_i2c_delay_us();
	soft_i2c_set_sda_level(soft_i2c,0);
	soft_i2c_delay_us();
	soft_i2c_set_scl_level(soft_i2c,0);
}

/**
  * @brief  I2C产生停止信号(STOP)
  * @details 停止条件:SCL高时,SDA从低变高
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @retval 无
  */
void soft_i2c_stop(SOFT_I2C_TypeDef soft_i2c)
{	
	soft_i2c_set_scl_level(soft_i2c,0);
	soft_i2c_set_sda_level(soft_i2c,0);
	soft_i2c_delay_us();
	soft_i2c_set_scl_level(soft_i2c,1);
	soft_i2c_set_sda_level(soft_i2c,1);
	soft_i2c_delay_us();
}

/**
  * @brief  等待I2C应答信号到来
  * @details 在第9个时钟周期时,从机拉低SDA表示应答
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @retval 0: 接收应答成功
  * @retval 1: 接收应答失败(超时)
  */
uint8_t soft_i2c_wait_ack(SOFT_I2C_TypeDef soft_i2c)
{
    uint8_t ucErrTime=0;
	soft_i2c_set_sda_level(soft_i2c,1);	 
    soft_i2c_delay_us();
	soft_i2c_set_scl_level(soft_i2c,1);	 
    soft_i2c_delay_us();
    while(soft_i2c_get_sda_level(soft_i2c))
    {
        ucErrTime++;
        if(ucErrTime>250)
        {
            soft_i2c_stop(soft_i2c);
            return 1;
        }
    } 
	soft_i2c_set_scl_level(soft_i2c,0);	 
    return 0;
}

/**
  * @brief  主机产生I2C应答信号(ACK)
  * @details 拉低SDA,在SCL高期间保持
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @retval 无
  */
void soft_i2c_ack(SOFT_I2C_TypeDef soft_i2c)
{	
	soft_i2c_set_sda_level(soft_i2c, 0);  // 拉低 SDA(ACK 信号)
    soft_i2c_delay_us();
    soft_i2c_set_scl_level(soft_i2c, 1);  // 拉高 SCL(从机读取 ACK)
    soft_i2c_delay_us();
    soft_i2c_set_scl_level(soft_i2c, 0);  // 拉低 SCL(结束第 9 个时钟周期)
    
    // 【关键】释放 SDA,设为高电平(或输入模式),让从机可以发送下一个字节
    soft_i2c_set_sda_level(soft_i2c, 1);  
    soft_i2c_delay_us();
}

/**
  * @brief  主机产生I2C非应答信号(NACK)
  * @details 保持SDA高电平,在SCL高期间释放
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @retval 无
  */
void soft_i2c_nack(SOFT_I2C_TypeDef soft_i2c)
{
	soft_i2c_set_sda_level(soft_i2c, 1);  // 保持 SDA 高电平(NACK)
    soft_i2c_delay_us();
    soft_i2c_set_scl_level(soft_i2c, 1);  // 拉高 SCL
    soft_i2c_delay_us();
    soft_i2c_set_scl_level(soft_i2c, 0);  // 拉低 SCL
    // NACK 后通常跟着 STOP,所以这里可以不操作 SDA,由 stop 函数处理
}

/**
  * @brief  I2C发送一个字节数据
  * @details 高位先发(MSB)
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @param  txd: 待发送字节数据
  * @retval 无
  */
void soft_i2c_send_byte(SOFT_I2C_TypeDef soft_i2c,uint8_t txd)
{
    uint8_t i;  
	soft_i2c_set_scl_level(soft_i2c,0);	
    for (i = 0; i < 8; i++)
    {
		if((txd&0x80)>>7)
			soft_i2c_set_sda_level(soft_i2c,1);			
		else
			soft_i2c_set_sda_level(soft_i2c,0);	
        txd<<=1;
		soft_i2c_set_scl_level(soft_i2c,1);
        soft_i2c_delay_us();  
		soft_i2c_set_scl_level(soft_i2c,0);
        soft_i2c_delay_us();  
    }
}

/**
  * @brief  I2C读取一个字节数据
  * @details 高位先读(MSB)
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @param  ack: 读取后是否产生应答
  *   @arg  1: 产生应答(继续接收)
  *   @arg  0: 不产生应答(结束接收)
  * @retval 读取的字节数据
  */
uint8_t soft_i2c_read_byte(SOFT_I2C_TypeDef soft_i2c,uint8_t ack)
{
    uint8_t i,receive = 0;  

    for (i = 0; i < 8; i++)
    {
		soft_i2c_set_scl_level(soft_i2c,0);
        soft_i2c_delay_us();  
		soft_i2c_set_scl_level(soft_i2c,1);
        receive <<= 1; 
        if (soft_i2c_get_sda_level(soft_i2c))
        {
            receive++; 
        }
        soft_i2c_delay_us();  
    }
	
	soft_i2c_set_scl_level(soft_i2c, 0);
    soft_i2c_delay_us();
	
    if (!ack)
        soft_i2c_nack(soft_i2c); //非应答
    else
        soft_i2c_ack(soft_i2c); //应答

    return receive;
}

/**
  * @brief  从指定I2C设备的寄存器读取一个字节
  * @details I2C通信流程:START -> 发送从机地址(写) -> 发送寄存器地址 -> 
  *          重复START -> 发送从机地址(读) -> 读取数据 -> NACK -> STOP
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @param  addr: I2C设备地址(7位,不包含读写位)
  * @param  reg: 设备寄存器地址
  * @param  data: 指向数据保存缓冲区的指针
  * @retval 0: 读取成功
  * @retval 1: 读取失败
  */
uint8_t soft_i2c_read_dev_one_byte(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,uint8_t reg,uint8_t *data)
{
    soft_i2c_start(soft_i2c);
    soft_i2c_send_byte(soft_i2c,(addr<<1)|0);     
    if(soft_i2c_wait_ack(soft_i2c))               
    {
        soft_i2c_stop(soft_i2c);
        return 1;
    }
    soft_i2c_send_byte(soft_i2c,reg);             
    if(soft_i2c_wait_ack(soft_i2c))               
    {
        soft_i2c_stop(soft_i2c);
        return 1;
    }
    soft_i2c_start(soft_i2c);                     
    soft_i2c_send_byte(soft_i2c,(addr<<1)|1);     
    if(soft_i2c_wait_ack(soft_i2c))               
    {
        soft_i2c_stop(soft_i2c);
        return 1;
    }
    *data = soft_i2c_read_byte(soft_i2c,0);    
    soft_i2c_stop(soft_i2c);

    return 0;
}

/**
  * @brief  向指定I2C设备的寄存器写入一个字节
  * @details I2C通信流程:START -> 发送从机地址(写) -> 发送寄存器地址 -> 
  *          发送数据字节 -> STOP
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @param  addr: I2C设备地址(7位,不包含读写位)
  * @param  reg: 设备寄存器地址
  * @param  data: 待写入的字节数据
  * @retval 0: 写入成功
  * @retval 1: 写入失败
  */
uint8_t soft_i2c_write_dev_one_byte(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,uint8_t reg,uint8_t data)
{
    soft_i2c_start(soft_i2c);
    soft_i2c_send_byte(soft_i2c,(addr<<1)|0);      
    if(soft_i2c_wait_ack(soft_i2c))               
    {
        soft_i2c_stop(soft_i2c);
        return 1;
    }
    soft_i2c_send_byte(soft_i2c,reg);             
    if(soft_i2c_wait_ack(soft_i2c))                
    {
        soft_i2c_stop(soft_i2c);
        return 1;
    }
    soft_i2c_send_byte(soft_i2c,data);             
    if(soft_i2c_wait_ack(soft_i2c))                
    {
        soft_i2c_stop(soft_i2c);
        return 1;
    }
    soft_i2c_stop(soft_i2c);
    return 0;
}

/**
  * @brief  从指定I2C设备的寄存器读取多个字节
  * @details I2C通信流程:START -> 发送从机地址(写) -> 发送寄存器地址 -> 
  *          重复START -> 发送从机地址(读) -> 连续读取len个字节 -> 
  *          最后一个字节后NACK -> STOP
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @param  addr: I2C设备地址(7位,不包含读写位)
  * @param  reg: 设备寄存器地址
  * @param  len: 读取字节数
  * @param  buf: 指向数据保存缓冲区的指针
  * @retval 0: 读取成功
  * @retval 1: 读取失败
  */
uint8_t soft_i2c_read_dev_len_byte(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf)
{
    soft_i2c_start(soft_i2c);
    soft_i2c_send_byte(soft_i2c,(addr<<1)|0);  
    if(soft_i2c_wait_ack(soft_i2c))          
    {
        soft_i2c_stop(soft_i2c);
        return 1;
    }
    soft_i2c_send_byte(soft_i2c,reg); 
    if(soft_i2c_wait_ack(soft_i2c))                
    {
        soft_i2c_stop(soft_i2c);
        return 1;
    }
    soft_i2c_start(soft_i2c);
    soft_i2c_send_byte(soft_i2c,(addr<<1)|1); 
    if(soft_i2c_wait_ack(soft_i2c))                
    {
        soft_i2c_stop(soft_i2c);
        return 1;
    }
    while(len)
    {
        if(len==1)*buf=soft_i2c_read_byte(soft_i2c,0);
        else *buf=soft_i2c_read_byte(soft_i2c,1);		
        len--;
        buf++;
    }
    soft_i2c_stop(soft_i2c);                
    return 0;
}

/**
  * @brief  向指定I2C设备的寄存器写入多个字节
  * @details I2C通信流程:START -> 发送从机地址(写) -> 发送寄存器地址 -> 
  *          连续发送len个字节,每个字节后等待应答 -> STOP
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @param  addr: I2C设备地址(7位,不包含读写位)
  * @param  reg: 设备寄存器地址
  * @param  len: 写入字节数
  * @param  buf: 指向待写入数据的缓冲区指针
  * @retval 0: 写入成功
  * @retval 1: 写入失败
  */
uint8_t soft_i2c_write_dev_len_byte(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf)
{
    uint8_t i;
    soft_i2c_start(soft_i2c);
    soft_i2c_send_byte(soft_i2c,(addr<<1)|0);  
    if(soft_i2c_wait_ack(soft_i2c))         
    {
        soft_i2c_stop(soft_i2c);
        return 1;
    }
    soft_i2c_send_byte(soft_i2c,reg); 
    if(soft_i2c_wait_ack(soft_i2c))               
    {
        soft_i2c_stop(soft_i2c);
        return 1;
    }
    for(i=0; i<len; i++)
    {
        soft_i2c_send_byte(soft_i2c,buf[i]);  
        if(soft_i2c_wait_ack(soft_i2c))          
		{
			soft_i2c_stop(soft_i2c);
			return 1;
		}
    }
    soft_i2c_stop(soft_i2c);
    return 0;
}

/**
  * @brief  向I2C设备写入数据(简化接口)
  * @details I2C通信流程:START -> 发送从机地址(写) -> 连续发送len个字节 ->
  *          每个字节后等待应答 -> STOP
  *          相比 soft_i2c_write_dev_len_byte,此函数不需要指定寄存器地址
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @param  addr: I2C设备地址(7位,不包含读写位)
  * @param  buf: 指向待写入数据的缓冲区指针
  * @param  len: 写入字节数
  * @retval 0: 写入成功
  * @retval 1: 写入失败
  */
uint8_t soft_i2c_write(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,const uint8_t *buf,uint8_t len)
{
    uint8_t i;

    soft_i2c_start(soft_i2c);

    soft_i2c_send_byte(soft_i2c, (addr << 1) | 0);
    if (soft_i2c_wait_ack(soft_i2c))
        goto err;

    for (i = 0; i < len; i++)
    {
        soft_i2c_send_byte(soft_i2c, buf[i]);
        if (soft_i2c_wait_ack(soft_i2c))
            goto err;
    }

    soft_i2c_stop(soft_i2c);
    return 0;

err:
    soft_i2c_stop(soft_i2c);
    return 1;
}

/**
  * @brief  从I2C设备读取数据(简化接口)
  * @details I2C通信流程:START -> 发送从机地址(读) -> 连续读取len个字节 ->
  *          最后一个字节后NACK -> STOP
  *          相比 soft_i2c_read_dev_len_byte,此函数不需要指定寄存器地址
  * @param  soft_i2c: I2C编号
  *   @arg  SOFT_I2C1: 软件I2C1
  *   @arg  SOFT_I2C2: 软件I2C2
  * @param  addr: I2C设备地址(7位,不包含读写位)
  * @param  buf: 指向数据保存缓冲区的指针
  * @param  len: 读取字节数(必须大于0)
  * @retval 0: 读取成功
  * @retval 1: 读取失败或长度为0
  * @note   当len为0时返回失败
  */
uint8_t soft_i2c_read(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,uint8_t *buf,uint8_t len)
{
    uint8_t i;

    if (len == 0)
        return 1;

    soft_i2c_start(soft_i2c);

    soft_i2c_send_byte(soft_i2c, (addr << 1) | 1);
    if (soft_i2c_wait_ack(soft_i2c))
        goto err;

    for (i = 0; i < len; i++)
    {
        if (i == (len - 1))
            buf[i] = soft_i2c_read_byte(soft_i2c, 0); // NACK
        else
            buf[i] = soft_i2c_read_byte(soft_i2c, 1); // ACK
    }

    soft_i2c_stop(soft_i2c);
    return 0;

err:
    soft_i2c_stop(soft_i2c);
    return 1;
}

bsp_soft_i2c.h

#ifndef _BSP_SOFT_I2C_H
#define _BSP_SOFT_I2C_H

#include <stdint.h>

typedef enum {
	SOFT_I2C1 = 0,
	SOFT_I2C2 ,
}SOFT_I2C_TypeDef;

void soft_i2c_init(SOFT_I2C_TypeDef soft_i2c);
void soft_i2c_start(SOFT_I2C_TypeDef soft_i2c);
void soft_i2c_stop(SOFT_I2C_TypeDef soft_i2c);
uint8_t soft_i2c_wait_ack(SOFT_I2C_TypeDef soft_i2c);
void soft_i2c_ack(SOFT_I2C_TypeDef soft_i2c);
void soft_i2c_nack(SOFT_I2C_TypeDef soft_i2c);
void soft_i2c_send_byte(SOFT_I2C_TypeDef soft_i2c,uint8_t txd);
uint8_t soft_i2c_read_byte(SOFT_I2C_TypeDef soft_i2c,uint8_t ack);

uint8_t soft_i2c_write_dev_one_byte(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,uint8_t reg,uint8_t data);
uint8_t soft_i2c_read_dev_one_byte(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,uint8_t reg,uint8_t *data);
uint8_t soft_i2c_write_dev_len_byte(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf);
uint8_t soft_i2c_read_dev_len_byte(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf);

uint8_t soft_i2c_write(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,const uint8_t *buf,uint8_t len);
uint8_t soft_i2c_read(SOFT_I2C_TypeDef soft_i2c,uint8_t addr,uint8_t *buf,uint8_t len);

#endif
posted @ 2026-01-31 11:51  少年-潜行  阅读(0)  评论(0)    收藏  举报