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 |
---|---|---|---|---|
-- | COMMAND | W | Specifies Register Address | 0x00 |
0x00 | ENABLE | R/W | Enables states and interrupts | 0x00 |
0x01 | CONFIG | R/W | ALS gain and integration time configuration | 0x00 |
0x04 | AILTL | R/W | ALS interrupt low threshold low byte | 0x00 |
0x05 | AILTH | R/W | ALS interrupt low threshold high byte | 0x00 |
0x06 | AIHTL | R/W | ALS interrupt high threshold low byte | 0x00 |
0x07 | AIHTH | R/W | ALS interrupt high threshold high byte | 0x00 |
0x08 | NPAILTL | R/W | No Persist ALS interrupt low threshold low byte | 0x00 |
0x09 | NPAILTH | R/W | No Persist ALS interrupt low threshold high byte | 0x00 |
0x0A | NPAIHTL | R/W | No Persist ALS interrupt high threshold low byte | 0x00 |
0x0B | NPAIHTH | R/W | No Persist ALS interrupt high threshold high byte | 0x00 |
0x0C | PERSIST | R/W | Interrupt persistence filter | 0x00 |
0x11 | PID | R | Package ID | -- |
0x12 | ID | R | Device ID | ID |
0x13 | STATUS | R | Device status | 0x00 |
0x14 | C0DATAL | R | CH0 ADC low data byte | 0x00 |
0x15 | C0DATAH | R | CH0 ADC high data byte | 0x00 |
0x16 | C1DATAL | R | CH1 ADC low data byte | 0x00 |
0x17 | C1DATAH | R | CH1 ADC high data byte | 0x00 |
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:
|
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:
|
Reserved | 3 | Reserved. Write as 0. |
ATIME | 2:0 | ALS Integration Time. Sets the ADC integration period for both channels:
|
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:
|
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 |