CH32V006K8U6 with TSL2591 Light Sensor

In this article, I show you how to connect and operate the TSL2591 light sensor with a CH32 microcontroller. I picked the CH32V006K8U6 because I recently made a development board with it, and I kind of want to advertise it, plus it is a more powerful microcontroller with more memory than the CH32V003F4P6, which I will need for the project that I will build upon this knowledge. Also, I will use a bit “more advanced” I2C communication for this sensor. I wrote some more advanced functions, both for reading and writing, than the basic I2C functions, which can be useful not only for this I2C chip but for many others.

Introduction

This light sensor is a dual-diode light sensor with I2C connectivity. It has programmable gain and integration time. One of the photodiodes is an IR diode (CH1), and the other one is a regular full-spectrum diode (CH0). Each diode has its own ADC, and its conversion results are stored in the corresponding data registers, which can then be read through the I2C bus. To avoid continuous polling, the sensor also has an interrupt feature. When a “meaningful light intensity change” occurs, the interrupt fires and then a conversion can be performed. This “meaningful light intensity change” can be defined by the user both in terms of light intensity and time. The user can define two sets of thresholds, both above and below the current light level, and when the measured values exceed these limits, interrupts are generated. The interrupt can fire immediately (“No Persist ALS Interrupt”) or after the thresholds are exceeded N times in a row (“ALS Interrupt”).

The chip operates at 3.3 V, and it has a maximum current consumption of 275 μA. However, in power-saving sleep mode, this current consumption can be reduced to 2.3 μA. Perfect for low-power applications using batteries. The device has a fixed 0x29 I2C address.

The response of the chip to light sources is different. It has about half the responsivity for infrared light as for regular light in the visible spectrum. The same applies to the angular response, too. However, when it comes to IR light, the chip is less sensitive to the incident angle than for regular light.

The hardware connection is fairly simple. The chip requires a 1 μF low-ESR decoupling capacitor for its supply line and some pull-up resistors for the I2C line (1.5 kOhm) and the interrupt line (10 kOhm).

 

TSL2591 closeup picture. The checkered pattern is the chips’s photodiode array area.

 

I2C functions

Before working with the sensor itself, we need to do some groundwork. I had to make my own “receive bytes” and “send bytes” functions based on the CH32’s I2C functions so I can send and receive multiple bytes at a time.

I realised that my bit-banged I2C did not really work the way I wanted to when I had to send or receive multiple bytes consecutively. So, I went back to my I2C basics code and looked at how to modify the single-byte I2C transfers so that they can do multiple bytes as well.

I could almost reuse the same code, I just had to encapsulate the I2C_SendData() and I2C_ReceiveData() functions in some for() loops so they send and receive the bytes properly.

 
for(uint8_t n=0; n < numberOfBytes; n++)
{
   if( I2C_GetFlagStatus(I2C1, I2C_FLAG_TXE) !=  RESET) 
    {
        I2C_SendData( I2C1, Buffer[n]);
        while( !I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    }
}
for(uint8_t n=0; n<numberOfBytes; n++)
{
    while(!I2C_GetFlagStatus(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED | I2C_FLAG_BTF)); 
    Delay_Us(1);

    if(I2C_GetFlagStatus(I2C1, I2C_FLAG_RXNE) != RESET) 
    {
        Buffer[n] = I2C_ReceiveData(I2C1); 
    }
}
 

Registers

All the registers and their purposes are explained in the datasheet, so for a full picture, please read it. In this section, I will just focus on those which are relevant for this demonstration.

Each registers have an address shown in HEX code. So, basically, one can just create a macro for all the addresses with the name of the register and store it in a corresponding header file. In my code, I did exactly that. I copied all the registers and made a macro for them. This way, it is easy to find them and refer to them.

#define TSL2591_COMMAND_BIT 0xA0   
#define TSL2591_ENABLE_REG 0x00    
#define TSL2591_CONFIG_REG 0x01    
#define TSL2591_AILTL 0x04          
#define TSL2591_AILTH 0x05       
#define TSL2591_AIHTL 0x06          
#define TSL2591_AIHTH 0x07         
#define TSL2591_NPAILTL 0x08      
#define TSL2591_NPAILTH 0x09      
#define TSL2591_NPAIHTL 0x0A     
#define TSL2591_NPAIHTH 0x0B       
#define TSL2591_PERSIST 0x0C       
#define TSL2591_PackageID 0x11      
#define TSL2591_DeviceID 0x12       
#define TSL2591_STATUS 0x13       
#define TSL2591_C0DATAL  0x14      
#define TSL2591_C0DATAH  0x15       
#define TSL2591_C1DATAL  0x16       
#define TSL2591_ C1DATAH  0x17      
 
Address Register Name R/W Register Function Reset Value
--COMMANDWSpecifies Register Address0x00
0x00ENABLER/WEnables states and interrupts0x00
0x01CONFIGR/WALS gain and integration time configuration0x00
0x04AILTLR/WALS interrupt low threshold low byte0x00
0x05AILTHR/WALS interrupt low threshold high byte0x00
0x06AIHTLR/WALS interrupt high threshold low byte0x00
0x07AIHTHR/WALS interrupt high threshold high byte0x00
0x08NPAILTLR/WNo Persist ALS interrupt low threshold low byte0x00
0x09NPAILTHR/WNo Persist ALS interrupt low threshold high byte0x00
0x0ANPAIHTLR/WNo Persist ALS interrupt high threshold low byte0x00
0x0BNPAIHTHR/WNo Persist ALS interrupt high threshold high byte0x00
0x0CPERSISTR/WInterrupt persistence filter0x00
0x11PIDRPackage ID--
0x12IDRDevice IDID
0x13STATUSRDevice status0x00
0x14C0DATALRCH0 ADC low data byte0x00
0x15C0DATAHRCH0 ADC high data byte0x00
0x16C1DATALRCH1 ADC low data byte0x00
0x17C1DATAHRCH1 ADC high data byte0x00
 

Command register

When we want to use the device, we need to first send a byte to the command register. In normal operation, we simply send the 0xA0 (0b10100000) to the device, which selects the command register and puts the device into normal operation mode. Then, if this value (0xA0) is logically OR’d (“|”) together with a register address, then this register address is targeted, and in the next byte, we can tell the device what to do (if it is a readout operation).

Sometimes we want to use the device in interrupt mode, which would require us to clear the interrupts from time to time. This requires sending another type of command. For example, when all interrupts should be cleared, the device must be put into special function mode.

Sending 0xE7 (0b11100111) would put the device into special function mode, and it would clear ALS and no-persist ALS interrupts.

void TSL2591_ClearInterrupt(void)
{
    uint8_t cmd = 0xE7;
    I2C_SendByte(0x29, &cmd, 1);
}
 
Field Bits Description
CMD 7 Select Command Register. Must write as 1 when addressing the COMMAND register.
TRANSACTION 6:5 Select type of transaction to follow in subsequent data transfers:
00 – Reserved (Do not use)
01 – Normal Operation
10 – Reserved (Do not use)
11 – Special Function
ADDR/SF 4:0 Address or Special Function field:
• In Normal mode: Selects the control/status/data register address.
• In Special Function mode: Issues a special function command:
  • 00100 – Interrupt set (forces interrupt)
  • 00110 – Clear ALS interrupt
  • 00111 – Clear ALS & no‑persist ALS interrupt
  • 01010 – Clear no‑persist ALS interrupt
Other values are reserved.
 

Enable register

Following the logic of the command register, if we combine 0xA0 with 0x00, the address of the enable register, then we can send commands to the device to turn it on/off or enable interrupts and functions.

In case we would enable the interrupts, it is important to first set the thresholds and (AILTL or NPAILTL) and then, in the last step, enable the corresponding functions in the register. Otherwise, we could enable the interrupts with invalid threshold values, and before setting the threshold values we want, the interrupt would be triggered continuously.

Also, another important detail is that the unit of the threshold values is the masked value of the full luminosity, which is the Channel 0 values of the AD conversion.

 
Field Bit(s) Description
NPIEN 7 No‑Persist Interrupt Enable.
When set, NP threshold conditions generate an interrupt immediately, bypassing the persistence filter.
SAI 6 Sleep After Interrupt.
When set, device powers down after an ALS cycle that triggers an interrupt.
Reserved 5 Reserved. Write as 0.
AIEN 4 ALS Interrupt Enable.
When set, ALS interrupts are allowed (subject to persistence filter).
Reserved 3:2 Reserved. Write as 0.
AEN 1 ALS Enable.
When set, enables the ALS (light sensing) function.
PON 0 Power ON.
When set, activates the internal oscillator and allows timers and ADCs to operate.
 

Control (config) register

If we combine 0xA0 with 0x01, we access the control register. Using this register, we can configure different gain (“AGAIN”) and integration time (“ATIME”) values. We can even reset the system. I made a simple function that sets the gain and integration time simultaneously:

void TSL2591_setGainAndIntegrationTime(tsl2591_gain_t gain, tsl2591_integration_time_t integrationTime)
{
    uint8_t tx[2];

    tx[0] = TSL2591_COMMAND_BIT | TSL2591_CONTROL_REG; 
    tx[1] = integrationTime | gain; 
    I2C_SendByte(0x29, tx, 2); 

    _integration = integrationTime; 
    _gain = gain;
}

Since both the gain and integration times are fixed values, we can created enumerations (named integer constants) for them. This way, it is quick and easy to pass values to the above-defined function. For example, the integration times are stored in the following way:

typedef enum {
    TSL2591_INTEGRATION_100MS = 0x00,
    TSL2591_INTEGRATION_200MS = 0x01,
    TSL2591_INTEGRATION_300MS = 0x02,
    TSL2591_INTEGRATION_400MS = 0x03,
    TSL2591_INTEGRATION_500MS = 0x04,
    TSL2591_INTEGRATION_600MS = 0x05
} tsl2591_integration_time_t;



 
Field Bit(s) Description
SRESET 7 System Reset.
When set to 1, performs a software reset equivalent to a power‑on reset.
Self‑clearing after execution.
Reserved 6 Reserved. Write as 0.
AGAIN 5:4 ALS Gain Control.
Sets the internal gain for CH0 and CH1:
  • 00 – Low gain (1×)
  • 01 – Medium gain (25×)
  • 10 – High gain (428×)
  • 11 – Maximum gain (9876×)
Reserved 3 Reserved. Write as 0.
ATIME 2:0 ALS Integration Time.
Sets the ADC integration period for both channels:
  • 000 – 100 ms (max count 37888)
  • 001 – 200 ms (max count 65535)
  • 010 – 300 ms (max count 65535)
  • 011 – 400 ms (max count 65535)
  • 100 – 500 ms (max count 65535)
  • 101 – 600 ms (max count 65535)
 

ALS interrupt threshold register and persist register

These two registers are used to set up the interrupt thresholds. They are a bit more complicated because each value has an upper and a lower byte. But thanks to the multi-byte I2C implementation it is not an issue to send the values to the sensor.

The ALS and No-persist ALS differ from each other only by the “trigger sensitivity”. The No-persist ALS interrupt fires immediately once the measured light value is outside any of the threshold values (low or high threshold). However, when the regular ALS thresholds are used, we must also define a persist value which defines how many consecutive out-of-range readings are required to raise an interrupt.

The normal ALS interrupt is better suited for stable ambient light monitoring because by defining the persist value, we can avoid false triggers from short light changes such as flickers or brief shadows.

On the other hand, the No-persist ALS is better suited for fast and transient light events such as flashes, strobes or sudden shadows because it gives an instant interrupt response without waiting multiple cycles.

void TSL2591_EnableALSInterrupt(uint16_t lowThreshold, uint16_t highThreshold, uint8_t persistence)
{
    uint8_t tx[5];  
  
    tx[0] = TSL2591_COMMAND_BIT | TSL2591_AILTL;
    tx[1] = lowThreshold  & 0xFF;     
    tx[2] = lowThreshold  >> 8;       
    tx[3] = highThreshold & 0xFF;    
    tx[4] = highThreshold >> 8;       
    I2C_SendByte(0x29, tx, 5);    
    
    tx[0] = TSL2591_COMMAND_BIT | TSL2591_PERSIST; 
    tx[1] = persistence & 0x0F; 
    I2C_SendByte(0x29, tx, 2);   

    tx[0] = TSL2591_COMMAND_BIT | TSL2591_ENABLE_REG;  
    tx[1] = TSL2591_PON | TSL2591_AEN | TSL2591_AIEN; 
    I2C_SendByte(0x29, tx, 2);
}

void TSL2591_EnableNoPersistInterrupt(uint16_t lowThreshold, uint16_t highThreshold)
{
    uint8_t tx[5];

    tx[0] = TSL2591_COMMAND_BIT | TSL2591_NPAILTL;
    tx[1] = lowThreshold  & 0xFF;
    tx[2] = lowThreshold  >> 8;
    tx[3] = highThreshold & 0xFF;
    tx[4] = highThreshold >> 8;
    I2C_SendByte(0x29, tx, 5);
    
    tx[0] = TSL2591_COMMAND_BIT | TSL2591_ENABLE_REG;
    tx[1] = TSL2591_PON | TSL2591_AEN | TSL2591_NPIEN;
    I2C_SendByte(0x29, tx, 2);
}
 
Register Address Bits Function Reset
AILTL 0x04 7:0 ALS Low Threshold – Low Byte.
Part of the 16‑bit CH0 low threshold value. LSB first.
0x00
AILTH 0x05 7:0 ALS Low Threshold – High Byte.
Upper 8 bits of the CH0 low threshold.
0x00
AIHTL 0x06 7:0 ALS High Threshold – Low Byte.
Part of the 16‑bit CH0 high threshold value. LSB first.
0x00
AIHTH 0x07 7:0 ALS High Threshold – High Byte.
Upper 8 bits of the CH0 high threshold.
0x00
NPAILTL 0x08 7:0 No‑Persist ALS Low Threshold – Low Byte.
Part of the 16‑bit CH0 low threshold value for the no‑persist interrupt. LSB first.
0x00
NPAILTH 0x09 7:0 No‑Persist ALS Low Threshold – High Byte.
Upper 8 bits of the CH0 no‑persist low threshold.
0x00
NPAIHTL 0x0A 7:0 No‑Persist ALS High Threshold – Low Byte.
Part of the 16‑bit CH0 high threshold value for the no‑persist interrupt. LSB first.
0x00
NPAIHTH 0x0B 7:0 No‑Persist ALS High Threshold – High Byte.
Upper 8 bits of the CH0 no‑persist high threshold.
0x00
PERSIST 0x0C 3:0 ALS Interrupt Persistence Filter.
Defines how many consecutive out-of-range ALS readings are required to trigger an interrupt:
  • 0000 – Every ALS cycle generates an interrupt
  • 0001 – Any value outside threshold range
  • 0010 – 2 consecutive values out of range
  • 0011 – 3 consecutive values out of range
  • 0100 – 5 consecutive values out of range
  • 0101 – 10 consecutive values out of range
  • 0110 – 15 consecutive values out of range
  • 0111 – 20 consecutive values out of range
  • 1000 – 25 consecutive values out of range
  • 1001 – 30 consecutive values out of range
  • 1010 – 35 consecutive values out of range
  • 1011 – 40 consecutive values out of range
  • 1100 – 45 consecutive values out of range
  • 1101 – 50 consecutive values out of range
  • 1110 – 55 consecutive values out of range
  • 1111 – 60 consecutive values out of range
0x00
 

ALS data register

This is the register that we need to read in order to get the luminosity data.

Since the sensor has 2 diodes (IR and full spectrum), there are two channels. Each channel’s conversion result is stored as two bytes, so we need to read the registers accordingly.

Since we use the auto-increment function of the chip when reading, first we read out C0DATAL, then we proceed to C1DATAL.

uint32_t TSL2591_GetFullLuminosity(void)
{
    Delay_Ms( (uint16_t)(_integration + 1) * 100 );
    
    uint8_t rx[2]; 
    uint8_t tx[1];

    tx[0] = TSL2591_COMMAND_BIT | TSL2591_C0DATAL; 
    I2C_SendByte(0x29, tx, 1);
    I2C_ReceiveByte(0x29, rx, 2);
    uint16_t full = ((uint16_t)rx[1] << 8) | rx[0]; 
    
    tx[0] = TSL2591_COMMAND_BIT | TSL2591_C1DATAL; 
    I2C_SendByte(0x29, tx, 1); 
    I2C_ReceiveByte(0x29, rx, 2); 
    uint16_t ir = ((uint16_t)rx[1] << 8) | rx[0];  
    
    uint32_t luminosity = ((uint32_t)ir << 16) | full; 

    return luminosity;
}

The full code can be found on my GitHub page:

 
Register Address Bits Function Reset
C0DATAL 0x14 7:0 CH0 (Full Spectrum) Data – Low Byte.
LSB of the 16‑bit CH0 ADC result. Read first to latch the high byte.
0x00
C0DATAH 0x15 7:0 CH0 (Full Spectrum) Data – High Byte.
MSB of the 16‑bit CH0 ADC result.
0x00
C1DATAL 0x16 7:0 CH1 (Infrared) Data – Low Byte.
LSB of the 16‑bit CH1 ADC result. Read first to latch the high byte.
0x00
C1DATAH 0x17 7:0 CH1 (Infrared) Data – High Byte.
MSB of the 16‑bit CH1 ADC result.
0x00

The TSL2591 sensor breakout board. Click the link for the affiliate product link.

 

If you found this content useful, please consider joining my YouTube channel’s membership or leaving a donation.

Also, please consider using my affiliate links when buying relevant gadgets.

Next
Next

Hawkeye Firefly Split V6-PRO - A comprehensive review