AS3935 Lightning Detector Application for Windows 10/11 - 2025.10.25
After reading the dramatic AS3935 Lightning Sensor exposé, you will undoubtedly be tempted to write a Windows application for AS3935 calibration, configuration and logging, since no such application exists. But to save time, you can use this one...
The AS3935 sensor is connected to a microcontroller card programmed as a USB-to-I2C gateway, allowing the Windows application to communicate with the AS3935 module via a USB connection.
Most microcontroller cards have a USB connection that looks like a standard COM port. The Windows application just sends and receives messages over the COM port, so no special USB programming is required. A simple binary communications protocol is used to talk to the USB-to-I2C gateway microcontroller.
The code in the gateway microcontroller knows nothing about the AS3935 - all it does is receive USB messages, pass them over the I2C connection and return the I2C response via USB. All the knowledge of AS3935 communications, registers and parameters is inside the Windows program. This was easy to do by translating the AS3935 driver code you can find here from C++ to C#, and replacing the readRegister() and writeRegister() methods to use the USB/COM communications instead of I2C.
AS3935 Data Sheet
This version of the AS3935 data sheet was used for development, v1-04 2016-Jan-13. All page references refer to this document. Here you can read the details of calibration and configuration, in particular how it handles 'disturbers' and 'noise'.
This application is written in C# using the [antique*] Windows Forms .NET framework. It runs on Windows 11, or Windows 10 with the .NET Framework v4.7.2 or later.
(* Despite being old, WinForms with C# remains the fastest, easiest and safest way to develop simple Windows desktop applications. Hours or days, instead of weeks or months. And I like C#.)
Download the executable, AS3935.exe, via the link at the end of this page.
The main window can be resized or maximized to increase the size of the Graphical View window which is rescaled according to the main window size.
NOTE! In the above screen shot you can see the 'AFE Gain' is set to maximum gain (31), and the 'Watchdog Threshold' and 'Spike Rejection' set to minimum (0). This is NOT a valid configuration, but it does let you see lightning strikes occasionally even if there is no lightning. Normally you should use the default values (press 'Set Defaults' then 'Write Changes').
Windows Application Executable, AS3935.exe
Click on this link to download the .exe file (60KB). Windows Defender will do a comprehensive anti-virus test on the file. It does not need installation, just run AS3935.exe and delete it if you don't need it.
When you run it, you will hopefully see the encouraging "Windows has protected your PC" message. This is only because the program does not have a Software Signing Certificate yet, so you get 'Unknown Publisher' - but you know the publisher, it's muman.ch. Press 'More Info', then 'Run Anyway'.
Choose the correct COM port from the drop-down list. The list is refreshed every second, so it will show the COM port when you connect the controller. You may see other COM ports here which are not the AS3935 device. To find out which is which, use the Window's 'Device Manager'. Right-click on the Start button and choose 'Device Manager' from the context menu, then expand the 'Ports (COM & LPT)' branch.
When connected, the red heart to the right of the 'USB Serial Port' text will by toggled on and off for every poll message sent to the sensor, so it pulses once a second while polling is running.
Connect/Disconnect button
Connects to the controller and starts polling the sensor. If already connected, it disconnects and stops polling. The sensor's settings, the Graphical View and the Event Log remain unchanged when you disconnect.
Action Buttons
Online Help button
Displays this page in your browser.
Load Settings... & Save Settings... buttons
The settings in the Calibration and Configuration groups can be saved and loaded to/from an INI file. This is useful because the sensor does not retain these settings (it has no non-volatile memory), so you must re-write the settings each time the sensor is powered up or reset. The default name of the INI file is the current date and time.
Sends a 'reset' command to the sensor, which restores its default settings. The app's Calibration and Configuration settings are refreshed with the new values.
Clear Statistics button
Clears the statistics built up by the lightning distance estimation algorithm block. The storm detection algorithm starts rebuilding its statistics.
Calibration Group
Refer to 'Antenna Tuning' section in the data sheet, p35.
Values are changed by clicking in the right-hand value column to open a drop-down list. The first click selects the row. The second click, or first click on a selected row, displays the list.
Settings in this group are used to calibrate the resonant frequency of the LCO antenna oscillator, which must resonate at 500kHz, +-3.5%.
To tune the oscillator you must measure the frequency at the IRQ output and adjust the 'Tuning Capacitor' value (0..120pF) to be as close to 500kHz as possible. To measure the frequency accurately you will need a digital oscilloscope, like the Digilent Analog Discovery 2. I tried using a digital multimeter with a frequency measurement setting, but this was nowhere near accurate enough.
The oscillator frequency can be measured on the IQR output by setting 'IRQ Output' to 'LCO Antenna Osc, 500kHz'. The actual frequency of the signal you will see on this output is divided by the 'LOC Frequency Divider' value. The default divider is 16, so you will see '500kHz / 16' which should be 31.25hKz.
IRQ Output
Selects the oscillator frequency which will be routed to the IRQ Output pin. Normally you will select 'LCO Antenna Osc, 500kHz' with the 'LCO Frequency Divider' of '16 (31.25kHz)' and adjust the Tuning Capacitor value to give 31.25kHz on this output. You can also select the other two oscillator frequencies (SRCO or TRCO) to be output, but that's not needed for calibration, and they are apparently derived from the LCO antenna oscillator anyway. Don't forget to set it back to 'Normal' so the IRQ Output can be used as an event indicator.
LCO Frequency Divider
The frequency output on the IRQ pin is divided by this setting. The default is 16, for 31.25kHz if 'LCO Antenna Osc, 500kHz' is selected.
Tuning Capacitor (0..120pF)
Adjust this value so the LCO oscillator frequency is as close as possible to 500kHz by measuring 31.25kHz (div. 16) on the IRQ output. Unfortunately the AS3935 does not remember the Tuning Capacitor setting. It gets set back to 0 on each power up or reset, so you'll need to program it on every start-up. My sensor needed 120pF, the maximum value, to get to almost to 500kHz / 31.25kHz. I think about 125pF would have been optimal.
Set Defaults button
Sets the default data values in the view. It does not write the values to the sensor. You must press 'Write Changes' to write the values into the sensor.
Refresh button
Re-reads and displays the actual values from the sensor.
Write Changes button
Writes the changes made to the data values into the sensor and automatically re-reads and displays them for validation.
Configuration Group
After tuning the antenna's resonant frequency, you can adjust the configuration settings according to the location and RF environment.
AFE Gain (0..31) p28
The gain of the 'Analog Front End' (AFE). This adjusts the sensitivity to lightning and also to noise and 'disturbers'. There are two recommended settings, '14 OUTDOOR' and '18 INDOOR'. But you can experiment with this, setting it to 31 (maximum gain) lets you detect lightning occasionally, even when there is none. The INDOOR setting has a higher sensitivity than OUTDOOR because of the attenuation of the walls when indoors.
'Disturbers' are man-made (machine-generated) interference signals which can disturb the lightning detection algorithm. You can adjust the 'Watchdog Threshold' and/or the 'Spike Rejection' if you see too many disturber events. Or you can disable the detection (of just the reporting?) of these events completely by setting the 'Disturber Enabled' option to 'No'.
Minimum Strikes (1,5,9,16)
The minimum number of strikes needed to generate the 'Lightning Detected' event. The default is 1.
Noise Floor Level (0..7) p30
The output signal of the AFE is used to measure the noise floor level. The noise floor is continuously compared to a reference voltage (noise threshold). Whenever the noise floor level crosses the noise threshold, the AS3935 issues an interrupt (INT_NH). This indicates that the AS3935 cannot operate properly due to background noise received by the antenna. Increase this level if you see any 'Noise' events. A noise event disables lightning detection for 1.5 seconds.
Watchdog Threshold (0..15) p28
The watchdog threshold level defines when the AS3935 enters signal verification mode. Increasing this threshold makes it less sensitive to disturber events, but also makes it less sensitive to weak signals from distant lightning events. Figure 40 on p29 shows the effect of the watchdog threshold on the detection radius.
Spike Rejection (0..15) p31
In addition to the Noise Floor Level and Watchdog Threshold, the Spike Rejection setting reduces sensitivity to disturber events, but also reduces sensitivity to lighting events. Figure 42 on p32 shows the effect of the Spike Rejection value on the detection radius.
Disturber Enabled (Yes/No)
Disturber events are normally reported via the IRQ pin or register. Set this to 'No' to prevent disturber reporting. This sets the MASK_DIST bit which masks disturber events.
Decreasing sensitivity will reduce disturber events, but that also reduces sensitivity to lightning, so you may want to just turn off disturber reporting. Indoors I get about 5 disturber events an hour, even with quite high Noise Floor, Watchdog Threshold and Spike Rejection values. These occur when an electrical device with crude mechanical switches turns on or off, like the dishwasher, oven or kettle.
Set Defaults button
Sets the default data values in the view. It does not write the values to the sensor. You must press 'Write Changes' to write the values into the sensor.
Refresh button
Re-reads and displays the actual values from the sensor.
Write Changes button
Writes the changes made to the data values into the sensor and automatically re-reads and displays them for validation.
Graphical View
This view has a selectable time scale which defines the duration of events that are shown. The duration is also affected by the width of the window, because the graphical view is rescaled according to the main window size. The view stores up to 1024 events, then clears and starts again. The vertical gray line shows when the trace was started, which is useful if no events are occurring.
Scale setting
The graphical view has four scale settings which affect the duration shown in the graphical view. The actual maximum time on the horizontal axis depends on the size of the window.
1 minute per division : Shows about 35 minutes in a maximised window
2 minutes per division : Shows about 70 minutes
5 minutes per division : Shows about 180 minutes
10 minutes per division : Shows about 350 minutes
Three event types (INT_x) are shown in the graphical view. See data sheet p34.
Lightning strike, INT_L
Shown as a vertical red line. The height of the line is proportional to the 'strike energy', 0..2000000 over the vertical range. A big strike has a longer line.
Disturber event, INT_D
Shown as a small blue dot at the bottom of the trace. 'Disturbers' are man-made (machine-generated) interference signals which can disturb the lightning detection. A disturber event disables lighting detection for 1.5 seconds. The 'Watchdog Threshold' and 'Spike Rejection' settings can be adjusted to reduce the number of disturber events. Or you can disable the notification of these events completely by setting the 'Disturber Enabled' option to 'No'.
Noise event, INT_NH
Shown as a small black dot at the bottom of the trace. Adjust the 'Noise Floor Level' sensitivity if noise is detected. Lightning detection is disabled for the duration of the noise event. INT_NH persists as long as the noise level is above the threshold.
Storm distance
The distance (the radius) is shown as a value in green at the top right of the graphical view, e.g. 6km. The storm distance is also shown as a green horizontal line which relates to the km scale on the right-hand side of the view. This shows if the storm is moving closer or moving away. The sensor does not change the reported storm distance unless a new value is calculated. This means the graphical view would keep showing the same distance indefinitely if no further events occur. To fix this there's a 10-minute timeout - the distance measurement is turned off if no strikes are detected for 10 minutes.
Clear Graph button
Clears the graphical display.
Event Log
This is a circular list of the last 1024 events from the sensor. It also shows user actions enclosed in <angle brackets>. Each entry has a time stamp showing the date and time.
Lightning strikes are shown with the distance in km and the strike energy value. Disturbers and Noise are also logged, which helps to configure it to exclude noise and disturbances.
The log is saved as a CSV file which can be loaded into Excel, or read back into the Event Log and Graphical View. The 'Load Log' feature is not implemented in v1.0.
Clear Log button
This probably clears the event log.
USB-to-I2C Converter
For the Windows app to talk to the I2C sensor via a USB connection, we need an intelligent gateway to translate a simple binary serial protocol into I2C messages. A 'small message protocol' is used for USB communications. The windows application is the 'master' and the gateway is the 'slave'.
I used an STM-32F103 Nucleo board as the gateway, with the AS3935 sensor mounted on an Arduino shield prototype board. The sensor needs the power connected (3.3V and GND) plus the two I2C lines SDA and SCL. The IRQ output can be connected to D2, but this is not used because the sensor is polled with getIRQSource() which reads the INT bits.
I2C Pull-ups: The AS3935 board already has the I2C pull-up resistors, both 10K. The SCL pull-up is on the board and the SDA pull-up is in the chip (see data sheet p24), so you don't need to add the I2C pull-up resistors. The data sheet states, "please do not use 4K7 ohm resistors on both I2CD and I2CL" (SDA and SCL). This means if the microcontroller has pull-ups fitted they should be removed. The STM32F103 Nucleo board does not have pull-ups fitted, so it's ok. I tried an Arduino Zero and it did not work because it had two 4K7 pull-ups fitted and I did not want to unsolder them - but apparently some Zeros do not have them.
Here's the prototype shield showing the power and I2C connections to the Gravity board. IRQ is connected to D2, but it's not used. The Gravity module is stuck to the prototype board using thick double-sided mounting tape.
The Small Message Protocol
A simple 'state machine' is used by the master and the slave to receive and validate the USB serial messages.
Each binary message sent by the master (the PC) has the format:
0 1 2 3 4... word |B7|C9|seq|len|data-bytes|csum|
B7|C9 = Start bytes. (Why use B7 C9 as the start bytes? You can use anything you like, but these seemed to be rare as consecutive bytes. It doesn't really matter because a state-machine is used to receive messages, so even if these bytes did occur in the data stream it would be unlikely to cause any problems.) seq = Sequence number, 00..FF, incremented for each message. Use this to check the response is for the message that was sent, by comparing the seq numbers. This is not really necessary for USB/COM, but for some transport systems it can be important. len = The length of the data-bytes. data-bytes = The message payload, max. 32 bytes. csum = A two-byte XOR-and-rotate checksum, LS byte first.
For the I2C gateway, the data-bytes (message payload) has this format:
0 1 2 3...
|cmd|i2cadds|txlen|txdata-bytes|rxlen|
cmd = A one-byte command, 01 = send I2C message. i2cadds = The destination I2C address of the sensor. txlen = The length of the I2C message to send. txdata-bytes = The I2C message, usually a register number (read/write) and optional data byte (write). rxlen = The number of bytes to be read from the sensor, either 0 or 1.
There is always an ACK/NAK response from the slave to the master, even if no response data is required.
Returned ACK response with data:
0 1 2 3 4 5... word
|B7|C9|seq|ack|len|data-bytes...|csum|
seq = The returned sequence number. The master compares this with the seq number of the message that was sent. ack = ACK/NAK response value, 0x00 = ACK, >0x00 = NAK. len = The number of data-bytes returned. This should match the bytes requested with rxlen. Always 1 or 0 for this app. data-bytes = The requested data bytes.
NAK response with NAK reason 1..255:
0 1 2 3 4 5 6 |B7|C9|seq|nak|len|csum|
nak = The NAK reason, e.g. 1 = invalid data-length, 2 = buffer overflow, ... see source code. len = The data length is always 0x00 in a NAK response because no data is returned. The data could hold a description of the error, but the NAK reason is good enough.
The csum checksum is a simple XOR-and-rotate checksum of the seq, len and data-bytes, starting with csum = 0xFFFF. This is fast and reasonably reliable because it retains data from every byte, whereas an additive checksum loses data through overflow. (A real polynomial CRC is best, of course, but CRCs can be slow on slow MCUs and use many precious bytes of memory for a look-up table.)
// 16-bit xor-and-rotate-right checksum of seq, len and data-bytes
This is the source code for the USB-to-I2C converter. It was developed for an STM-32F103 Nucleo board with built-in ST-LINK, but should run on most Arduino-style MCUs. Microsoft Visual Studio 2022 (free) with the Visual Micro Arduino IDE extension (about $20 for a single user non-commercial license) was used for the programming and debugging.
#pragma once
// AS3935 Lightning Sensor with I2C communications
// Copyright (C) muman.ch, 2025.10.16
// info@muman.ch
/*
This communicates with a translator running SmallMessageProtocolSlave()
with the SerialToI2C app on the Arduino.
For full details of this code, see the blog entry:
https://muman.ch/muman/index.htm?muman-as3935-lightning-sensor-app.htm
DATA SHEET
All page references refer to THIS version of the data sheet
https://www.sciosense.com/wp-content/uploads/2023/12/AS3935-Data-Sheet.pdf
GRAVITY / DFROBOT MODULE
https://www.bastelgarage.ch/gravity-as3935-lightning-distance-sensor
THE OFFICIAL LIBRARY
The Mattlabs code below has several improvements on the official library.
https://github.com/DFRobot/DFRobot_AS3935/tree/master
*/
using System.Diagnostics;
using System.Threading;
namespace LightningDetector_WinForms
{
public class AS3935LightningSensor
{
protected uint i2cAdds;
protected SmallMessageProtocolMaster master;
// Register bit masks taken from data sheet p19
// the registers themselves do not have names, so we use the hex register numbers
public enum MASK : uint
{
// 0x00 R/W
AFE_GB = 0b00111110, // AFE gain boost
PWD = 0b00000001, // 1=power down, 0=active
// 0x01 R/W
NF_LEV = 0b01110000, // Noise floor level
WDTH = 0b00001111, // Watchdog threshold
// 0x02 R/W
CL_STAT = 0b01000000, // Clear statistics, toggle high-low-high
MIN_NUM_LIGH = 0b00110000, // Minimum strikes for INT
SREJ = 0b00001111, // Spike rejection
//0x03 R/W
LCO_FDIV = 0b11000000, // Frequency divider for antenna tuning
MASK_DIST = 0b00100000, // Mask disturber INT
INT = 0b00001111, // Interrupt source
// 0x04 R
S_LIG_L = 0b11111111, // Strike energy LS byte (21-bit value)
// 0x05 R
S_LIG_M = 0b11111111, // Strike energy MS byte
// 0x06 R
S_LIG_MM = 0b00011111, // Strike energy MMS byte
// 0x07 R
DISTANCE = 0b00111111, // Distance estimation in km, p33
// 0x08 R/W
DISP_LCO = 0b10000000, // IRQ has LCO signal, 500kHz / LCO_FDIV
DISP_SRCO = 0b01000000, // IRQ has SRCO signal, 1.1MHz
DISP_TRCO = 0b00100000, // IRQ has TRCO signal, 32.768kHz
TUN_CAP = 0b00001111, // Capacitor LCO tuning, 0..120pF in 8pF steps
// 0x3A R
TRCO_CALIB_DONE = 0b10000000, // TRCO calibration done successfully
TRCO_CALIB_NOK = 0b01000000, // TRCO calibration failed
// 0x3B R
SRCO_CALIB_DONE = 0b10000000, // SRCO calibration done successfully
SRCO_CALIB_NOK = 0b01000000, // SRCO calibration failed
// Send direct command, write 0x96 to issue the command
PRESET_DEFAULT = 0x3C, // Set all registers to default values
CALIB_RCO = 0x3D // Calibrate internal RC oscillators, 2ms
};
// IRQ source, what set IRQ active, register 0x03:INT
public enum IRQ_SOURCE : uint
{
INT_NH = 0b0001, // Noise level too high
INT_D = 0b0100, // Disturber detected, disabled by MASK_DIST
INT_L = 0b1000, // Lightning detected, no. of events set by MIN_NUM_LIGH
INT_PURGE = 0b0000 // Distance estimation changed due to event purge, p34
};
// Optimal AFE gains for setAFEGain()
public enum AFEGAIN : uint
{
INDOOR = 0x12, // default
OUTDOOR = 0x0e
};
///////////////////////////////////////////////////////////////////////////////
// Control
// Power up, reset the chip, set all registers to default values
public bool begin(SmallMessageProtocolMaster master, uint i2cAddress)
{
this.master = master;
this.i2cAdds = i2cAddress;
// if powered down, power it up
byte data;
if (!readRegister(0x00, out data))
return false;
if ((data & (byte)MASK.PWD) != 0) {
if (!powerUp())
return false;
}
// set all registers to default values
//return reset();
return true;
}
// Set all registers to their default values
// always call calibrateOscillators() after reset()
// takes 2ms
public bool reset()
{
if (!writeRegister((uint)MASK.PRESET_DEFAULT, 0x96))
return false;
delay(2);
return true;
}
// Power up
// always call calibrateOscillators() after powerUp()
// takes 2ms
// p36
public bool powerUp()
{
if (!writeRegister(0x00, (uint)MASK.PWD, 0))
return false;
// LCO startup time after power up is 2ms
delay(2);
return true;
}
public bool powerDown()
{
return writeRegister(0x00, (uint)MASK.PWD, 1);
}
// Calibrate the internal SRCO and TRCO oscillators
// this must be done after every powerUp() or reset()
// takes 2ms
// p36
public bool calibrateOscillators()
{
// calibrate RCO command
if (!writeRegister((uint)MASK.CALIB_RCO, 0x96))
return false;
//TODO only if it is in power down mode?
// toggle DISP_SRCO, as described in data sheet p36
if (!writeRegister(0x08, (uint)MASK.DISP_SRCO, 1))
return false;
delay(2);
if (!writeRegister(0x08, (uint)MASK.DISP_SRCO, 0))
return false;
delay(2);
// did it work? test the CALIB_DONE and CALIB_NOK flags
byte trco, srco;
if (!readRegister(0x3a, out trco) || !readRegister(0x3b, out srco))
return false;
// calibration should have been done
if ((trco & (uint)MASK.TRCO_CALIB_DONE) == 0 ||
(srco & (uint)MASK.SRCO_CALIB_DONE) == 0) {
Debug.WriteLine("calib not done");
return false;
}
// calibration errors?
if ((trco & (uint)MASK.TRCO_CALIB_NOK) != 0 ||
(srco & (uint)MASK.SRCO_CALIB_NOK) != 0) {
Debug.WriteLine("calib failed");
return false;
}
return true;
}
// Clear statistics for lightning distance estimation over the last 15 minutes
// this sets the getStormDistance() value to 0x3f (out of range)
// it does NOT affect the getStrikeEnergy() value
// p35
public bool clearStatistics()
{
// toggle CL_STAT bit high-low-high
// (this code reads register 0x02 just once instead of three times)
byte b;
if (!readRegister(0x02, out b))
return false;
if (!writeRegister(0x02, (byte)(b | (byte)MASK.CL_STAT)))
return false;
if (!writeRegister(0x02, (byte)(b & ~(byte)MASK.CL_STAT)))
return false;
return writeRegister(0x02, (byte)(b | (byte)MASK.CL_STAT));
}
// Very naughty, but I HATE await
public void delay(int ms)
{
Thread.Sleep(ms);
}
///////////////////////////////////////////////////////////////////////////////
// Data Values
// Returns the estimated distance (the radius) to the leading edge of
// the storm in kilometers : 1 = overhead .. 40 = 40km
// 63 (0x3f) = out of range or no storm detected
// The value remains unchanged until a new estimate is ready, or until
// clearStatistics() is called.
// p33 figure. 43
public bool getStormDistance(out uint km)
{
return readRegister(0x07, (uint)MASK.DISTANCE, out km);
}
// Returns the estimated energy of the latest lightning strike as
// a 21-bit value, 0..0x1fffff (0..2'097'151)
// It's a unitless magnitude, useful to compare with previous values.
// The value remains unchanged until a new strike occurs or it's set
// to 0 when a Disturber Event is detected. It is NOT cleared by
// clearStatistics().
// p32
public bool getStrikeEnergy(out uint strikeEnergy)
{
Debug.Assert(sizeof(uint) == 4);
strikeEnergy = 0;
byte lsByte, msByte, mmsByte;
if (!readRegister(0x04, out lsByte))
return false;
if (!readRegister(0x05, out msByte))
return false;
if (!readRegister(0x06, out mmsByte))
return false;
strikeEnergy = (uint)(((mmsByte & (byte)MASK.S_LIG_MM) << 16) +
(msByte << 8) + lsByte);
return true;
}
// If the IRQ output is activated, wait 2ms, then use this to find the
// source of the interrupt.
// Returns and IRQ_SOURCE value,
// 1 = INT_NH Noise level too high
// 4 = INT_D Disturber detected, disabled by MASK_DIST
// 8 = INT_L Lightning detected, no. of events set by MIN_NUM_LIGH
// 0 = INT_PURGE Distance estimation changed due to clearStatistics()
// call, see p34. 0 is only valid if IRQ goes high.
// The INT value is set to zero when it is read.
// p34
public bool getIRQSource(out uint irqSource)
{
return readRegister(0x03, (uint)MASK.INT, out irqSource);
}
///////////////////////////////////////////////////////////////////////////////
// Configuration
// Adjusts the AFE gain boost for indoor or outdoor use
// AFEGAIN::INDOOR (default) or AFEGAIN::OUTDOOR are the recommended
// settings, but any value between 0x00 and 0x1F can be used
// p28
public bool setAFEGain(AFEGAIN afeGain)
{
return writeRegister(0x00, (uint)MASK.AFE_GB, (uint)afeGain);
}
public bool getAFEGain(out AFEGAIN afeGain)
{
uint data;
bool ok = readRegister(0x00, (uint)MASK.AFE_GB, out data);
afeGain = (AFEGAIN)data;
return ok;
}
// Set the minimum number of lightning events in a 15 minute window
// to set IRQ and generate the INT_L interrupt, 0..3=1|5|9|16
// minStrikes : 0 = 1 strike (default); 1 = 5 strikes;
// 2 = 9 strikes; 3 = 16 strikes
public bool setMinimumStrikes(uint minStrikes)
{
return writeRegister(0x02, (uint)MASK.MIN_NUM_LIGH, minStrikes);
}
public bool getMinimumStrikes(out uint minStrikes)
{
return readRegister(0x02, (uint)MASK.MIN_NUM_LIGH, out minStrikes);
}
// Enable/disable the "disturber" interrupt INT_D with the
// MASK_DIST bit, 0=enabled, 1=masked and disabled
// p34
public bool setDisturberEnabled(bool enabled)
{
return writeRegister(0x03, (uint)MASK.MASK_DIST, enabled ? 0u : 1u);
}
public bool getDisturberEnabled(out bool enabled)
{
enabled = false;
if (!readRegister(0x03, (uint)MASK.MASK_DIST, out uint maskDist))
return false;
enabled = maskDist == 0 ? true : false;
return true;
}
// Selects the threshold for the Noise Floor Limit
// when the noise crosses this threshold it will issue the INT_NH
// interrupt and activate IRQ. This indicates that measurements
// cannot be taken because of excessive background noise.
// noiseFloorLevel : 0..7, level in microvolts RMS (default = 2)
// p30 figure 41
public bool setNoiseFloorLevel(uint noiseFloorLevel)
{
return writeRegister(0x01, (uint)MASK.NF_LEV, noiseFloorLevel);
}
public bool getNoiseFloorLevel(out uint noiseFloorLevel)
{
return readRegister(0x01, (uint)MASK.NF_LEV, out noiseFloorLevel);
}
// The watchdog threshold WDTH defines the signal level that causes
// entry to "Signal Verification Mode" where it determines if it's
// a strike or a disturber event, see p17.
// Increasing the threshold makes it less sensitive to disturbers,
// but also makes it less sensitive to weak signals from distant
// lightning sources.
// Sensitivity is also affected by SREJ, see setSpikeRejection().
// wdogThreshold : 0..15 (default = 2)
// p29 figure 40
public bool setWatchdogThreshold(uint wdogThreshold)
{
return writeRegister(0x01, (uint)MASK.WDTH, wdogThreshold);
}
public bool getWatchdogThreshold(out uint wdogThreshold)
{
return readRegister(0x01, (uint)MASK.WDTH, out wdogThreshold);
}
// Spike rejection works together with the watchdog threshold to
// determine if the signal is a lightning strike or a man-made
// disturber.
// Increasing the value improves disturber rejection, but also
// makes it less sensitive to weak signals from distant lightning
// sources.
// spikeRejection : 0..15 (default = 2)
// p31, p32 figure 42
public bool setSpikeRejection(uint spikeRejection)
{
return writeRegister(0x02, (uint)MASK.SREJ, spikeRejection);
}
public bool getSpikeRejection(out uint spikeRejection)
{
return readRegister(0x02, (uint)MASK.SREJ, out spikeRejection);
}
///////////////////////////////////////////////////////////////////////////////
// Calibration
// When calibrating the LCO antenna oscillator's resonant frequency,
// the oscillator frequency must be output on the IRQ pin so it can
// be accurately tuned to 500kHz +-3.5% with setTuningCapacitor().
// A frequency divider LCO_FDIV (16|32|64|128) for the LCO 500kHz is
// set with setLCOFrequencyDivider().
// n : 0 = return to normal IRQ operation
// 1 = output the LCO antenna oscillator frequency, 500kHz / LCO_FDIV
// 2 = output the SRCO system RC oscillator, 1.1MHz
// 3 = output the TRCO timer RC oscillator, 32.768kHz
// >>> DO NOT FORGET to call selectIRQOutput(0) to set the IRQ pin
// back to normal operation <<<
// You may also want to check the system RC oscillator or the timer
// RC oscillator frequencies too.
// p36
public bool setIRQOutput(uint n)
{
Debug.Assert(n < 4);
uint data = 0;
switch (n) {
case 1:
data = (uint)MASK.DISP_LCO;
break;
case 2:
data = (uint)MASK.DISP_SRCO;
break;
case 3:
data = (uint)MASK.DISP_TRCO;
break;
}
const uint mask = (uint)MASK.DISP_LCO | (uint)MASK.DISP_SRCO | (uint)MASK.DISP_TRCO;
return writeRegister(0x08, mask, data >> 5);
}
public bool getIRQOutput(out uint n)
{
n = 0;
const uint mask = (uint)MASK.DISP_LCO | (uint)MASK.DISP_SRCO | (uint)MASK.DISP_TRCO;
if (!readRegister(0x08, mask, out uint data))
return false;
// only 1 bit should be set
switch(data) {
case 0:
n = 0;
break;
case 1:
n = 3;
break;
case 2:
n = 2;
break;
case 4:
n = 1;
break;
default:
Debug.WriteLine("invalid IRQ output");
break;
}
return true;
}
// When tuning the antenna and outputing the LCO resonant frequency
// on the IRQ pin, see selectIRQOutput(), the LCO frequency is
// divided by 16|32|64|128 to give a lower frequency output.
// fdiv : 0 = divide by 16 (default); 1 = 32; 2 = 64; 3 = 128
// the LCO oscillator '500kHz / 16' should be 31.250kHz
// p35
public bool setLCOFrequencyDivider(uint fdiv)
{
return writeRegister(0x03, (uint)MASK.LCO_FDIV, fdiv);
}
public bool getLCOFrequencyDivider(out uint fdiv)
{
return readRegister(0x03, (uint)MASK.LCO_FDIV, out fdiv);
}
// The LCO antenna oscillator resonant frequency must be 500kHz +-3.5%.
// This tunes the resonant frequency by selecting the internal capacitors
// in steps of 8pf from 0..120pf. This varies the frequency by up to 25kHz.
// The resonant frequency can be verified by outputing the signal on the
// IRQ pin, see selectIRQOutput(). The frequency see on the pin is divided
// by LCO_FDIV, see setLOCFreuqncyDivider().
// tuneCap : 0 = 0pF; 1 = 8pF; 2 = 16pF; ..; 15 = 120pF
// p35
public bool setTuningCapacitor(uint tuneCap)
{
return writeRegister(0x08, (uint)MASK.TUN_CAP, tuneCap);
}
public bool getTuningCapacitor(out uint tuneCap)
{
return readRegister(0x08, (uint)MASK.TUN_CAP, out tuneCap);
}
///////////////////////////////////////////////////////////////////////////////
// Internal methods
// Reads the register bits defined by 'mask' and shifts them right
// (normalises) so the LS bit of *data is the LS bit of the mask.
public bool readRegister(uint reg, uint mask, out uint data)
{
data = 0;
Debug.Assert(mask != 0 && mask <= 0xff);
byte b;
if (!readRegister(reg, out b))
return false;
if (mask == 0xff) {
data = b;
return true;
}
// shift data according to the bit mask
b &= (byte)mask;
for (uint i = mask; (i & 1) == 0; i >>= 1) {
b >>= 1;
}
data = b;
return true;
}
// Writes register bits in 'mask' with 'newData' without modifying any other
// bits. 'newData' is shifted left to fit the mask. Returns false if the
// data will not fit the mask.
public bool writeRegister(uint reg, uint mask, uint newData)
{
Debug.Assert(mask != 0 && mask <= 0xff);
byte data;
if (mask == 0xff) {
// use all the bits
data = (byte)newData;
}
else {
// read existing value
if (!readRegister(reg, out data))
return false;
// shift new data according to the bit mask
for (uint i = mask; (i & 1) == 0; i >>= 1) {
newData <<= 1;
}
// make sure new data fits the bit mask
if ((newData & ~mask) != 0) {
Debug.WriteLine("data+mask error");
return false;
}
// mask out existing data and replace with new data
data = (byte)((data & ~mask) | newData);
}
// write the new data
return writeRegister(reg, data);
}
// Change these according to the comms protocol
// I2C write and get response
// 0 1 2 3...
// |01|i2cadds|txlen|txdata...|rxlen|
protected byte[] readRegMsg = { 0x01, 0xff, 0x01, 0xff, 0x01 };
protected byte[] writeRegMsg = { 0x01, 0xff, 0x02, 0xff, 0xff, 0x00 };
protected byte[] rxData = new byte[32];
protected bool readRegister(uint reg, out byte data)
{
data = 0;
readRegMsg[1] = (byte)i2cAdds;
readRegMsg[3] = (byte)reg;
if (!master.sendMessage(readRegMsg, 0, 5))
return false;
int rxDataLength, rxSeq, ackNak;
if (!master.getResponse(rxData, out rxDataLength, out rxSeq, out ackNak))
return false;
if (ackNak != 0) {
Debug.WriteLine("nak response " + ackNak);
return false;
}
if (rxDataLength != 1) {
Debug.WriteLine("rxDataLength != 1");
return false;
}
data = rxData[0];
return true;
}
protected bool writeRegister(uint reg, uint data)
{
writeRegMsg[1] = (byte)i2cAdds;
writeRegMsg[3] = (byte)reg;
writeRegMsg[4] = (byte)data;
if (!master.sendMessage(writeRegMsg, 0, 6))
return false;
int rxDataLength, rxSeq, ackNak;
if (!master.getResponse(rxData, out rxDataLength, out rxSeq, out ackNak))
return false;
if (ackNak != 0) {
Debug.WriteLine("nak response");
return false;
}
if (rxDataLength != 0) {
Debug.WriteLine("rxDataLength != 0");
return false;
}
return true;
}
#if DEBUG
public bool dumpRegisters()
{
byte b;
for (uint i = 0; i <= 8; ++i) {
if (!readRegister(i, out b))
return false;
Debug.WriteLine(string.Format("{0:X2} 0x{1:X2}", i, b));
}
for (uint i = 0x3a; i <= 0x3b; ++i) {
if (!readRegister(i, out b))
return false;
Debug.WriteLine(string.Format("{0:X2} 0x{1:X2}", i, b));
}
Debug.WriteLine("");
return true;
}
#endif
}
}
The full source code for the AS3935 Windows application (C# Windows Forms for Microsoft Visual Studio 2022) is available on request (for non-commercial use) from info@muman.ch.
Visual Micro Arduino IDE for Visual Studio 2022
Used for developing the USB-To-I2C converter. I recommend this, it's way better than the Arduino IDE. https://www.visualmicro.com/