Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ For the detailed description, please explore nested folders and corresponding CH
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [1.2.4] - 2026-08-31

### Added

### Fixed

- Acquisition PCB layout changes to improve RF performance
- Firmware improvements on nRF52 to improved reliability

### Changed

- Added new antenna (NN02-201) and corresponding matching network on Acquisition PCB

## [1.2.3] - 2026-04-02

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<img src="docs/images/wulpus_main.png" alt="WULPUS main" width="100%"/>

# WULPUS v1.2.3
# WULPUS v1.2.4
## Wearable Ultra Low-Power Ultrasound

# Introduction
Expand Down
15 changes: 15 additions & 0 deletions fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.2.4] - 2026-08-31

### Fixed

- Streaming could stop permanently, recoverable only by power cycling the probe. The interrupt that ends a frame's SPI burst had a small chance of being delayed by the SoftDevice; when delayed it allowed a fifth transfer, desynchronising the MSP430's fixed-size slave DMA. `DATA_READY` then stayed high, no further GPIOTE edge was produced, and both MCUs deadlocked with BLE still connected.
- I2C readout reliability improvements

### Changed

- `fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/us_spi.c`: Stop the SPI burst over PPI from the transfer counter's CC0 event rather than from its interrupt handler, removing the dependency on interrupt latency. Uses the previously declared but unwired `counter1_cc0_evt_addr`.
- `fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/main.c`: Clear both timers in `in_pin_handler()` so every frame starts from a known transfer count.
- `fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/pca10040/s132/config/sdk_config.h`: Raised GPIOTE and TIMER interrupt priority from 6 to 2, off the SoftDevice dispatcher's level.
- `fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/iis2dh.c`: Replaced six single-register accelerometer reads with one auto-incrementing burst read of `OUT_X_L..OUT_Z_H` and raised TWI from 100 kHz to 400 kHz, cutting main-loop blocking from roughly 2.4 ms to 0.2 ms per frame. Wire format unchanged.
- `fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/iis2dh.h`: Added `IIS2DH_AUTO_INCREMENT` and the `CTRL_REG4` bit definitions.

## [1.2.3] - 2026-04-02

### Added
Expand Down
150 changes: 93 additions & 57 deletions fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/iis2dh.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ static const nrf_drv_twi_t IIS2DH_twi = NRF_DRV_TWI_INSTANCE(TWI_INSTANCE_ID);

/* Flag to know when a I2C transfer has been completed */
static volatile bool IIS2DH_xfer_done = false;
/* Whether that transfer actually succeeded (false on address/data NACK) */
static volatile bool IIS2DH_xfer_ok = false;

extern volatile bool flag_add_IMU;

Expand All @@ -16,6 +18,25 @@ extern int buffer_counter;
extern int BLE_packet_ready;
extern ArrayList_type m_rx_buf[NUMBER_OF_XFERS*MAX_BUFFER_NUMBER_OF_US_FRAMES];

/* Layout of the accelerometer block inside a US frame.
*
* The six accelerometer bytes occupy the tail of the LAST packet of a frame, which is
* the tail of the 804-byte frame the host finally receives.
*
* The offset must keep the block inside that packet. buffer[] is BYTES_PR_XFER_RX long,
* and the byte one past its end is m_rx_buf[NUMBER_OF_XFERS*(k+1)].buffer[0] - the 0xFF
* start-of-frame marker of the NEXT frame. The dongle keys on that marker to find a frame
* boundary, so overwriting it makes the dongle discard all four packets of that frame.
*
* The previous offset (202 - ACC_BLOCK_BYTES) did exactly that: it clobbered the next
* frame's marker, dropped the sixth accelerometer byte off the end of the transmitted
* packet, and on the last ring slot wrote past m_rx_buf entirely. */
#define ACC_BLOCK_BYTES 6
#define ACC_BLOCK_OFFSET (BYTES_PR_XFER_RX - ACC_BLOCK_BYTES)

STATIC_ASSERT(ACC_BLOCK_OFFSET + ACC_BLOCK_BYTES <= BYTES_PR_XFER_RX,
"Accelerometer block overruns the last packet of the US frame");

uint8_t IIS2DH_buffer[805] = {};
uint16_t IIS2DH_buffer_index =0;
/* Frame counter for IIS2DH over bluetooth */
Expand All @@ -25,18 +46,12 @@ uint16_t IIS2DH_frame_number =0;
//Event Handler
static void twi_handler(nrf_drv_twi_evt_t const * p_event, void * p_context)
{
//Check the event to see what type of event occurred
switch (p_event->type)
{
//If data transmission or receiving is finished
case NRF_DRV_TWI_EVT_DONE:
IIS2DH_xfer_done = true;//Set the flag
break;

default:
// do nothing
break;
}
/* Every terminal event has to release the waiters, not just EVT_DONE.
* ADDRESS_NACK and DATA_NACK used to fall through without setting the flag,
* which left IIS2DH_register_read()/_write() spinning forever and took the
* whole main loop - and therefore the US stream - down with them. */
IIS2DH_xfer_ok = (p_event->type == NRF_DRV_TWI_EVT_DONE);
IIS2DH_xfer_done = true;
}


Expand All @@ -51,7 +66,7 @@ static void iis2dh_twi_init (void)
const nrf_drv_twi_config_t twi_config = {
.scl = PIN_IIS2DH_SCL,
.sda = PIN_IIS2DH_SDA,
.frequency = NRF_DRV_TWI_FREQ_100K,
.frequency = NRF_DRV_TWI_FREQ_400K,
.interrupt_priority = APP_IRQ_PRIORITY_LOWEST,
.clear_bus_init = false
};
Expand Down Expand Up @@ -82,33 +97,36 @@ bool IIS2DH_register_read(uint8_t register_address, uint8_t * rx_buffer, uint8_t
//Set the flag to false to show the receiving is not yet completed
IIS2DH_xfer_done = false;

// Send the register address where we want to read the data from
// Send the register address where we want to read the data from.
// Check the return code BEFORE waiting: if the transfer never started, no
// event will ever arrive and the wait below would never finish.
err_code = nrf_drv_twi_tx(&IIS2DH_twi, IIS2DH_ADDRESS, &register_address, 1, true);
if (NRF_SUCCESS != err_code)
{
return false;
}

//Wait for the transmission to be completed
while (IIS2DH_xfer_done == false){}

// If transmission was not successful, exit the function and return false
if (NRF_SUCCESS != err_code)
if (IIS2DH_xfer_ok == false)
{
return false;
}

//reset the flag so that we can read data from the IIS2DH's internal register
IIS2DH_xfer_done = false;

// Receive the data from the IIS2DH
err_code = nrf_drv_twi_rx(&IIS2DH_twi, IIS2DH_ADDRESS, rx_buffer, number_of_bytes);
//wait until the transmission is completed
while (IIS2DH_xfer_done == false){}

// if data was successfully read, return true else return false
if (NRF_SUCCESS != err_code)
{
return false;
}

return true;

//wait until the transmission is completed
while (IIS2DH_xfer_done == false){}

return IIS2DH_xfer_ok;
}
bool IIS2DH_register_write(uint8_t register_address, uint8_t * wx_buffer, uint8_t number_of_bytes)
{
Expand All @@ -124,18 +142,18 @@ bool IIS2DH_register_write(uint8_t register_address, uint8_t * wx_buffer, uint8_
wxbuffer_with_address[i+1] = wx_buffer[i];
}

// Send the register address where we want to read the data from
// Send the register address followed by the data (see the note in
// IIS2DH_register_read() on why err_code is checked before waiting).
err_code = nrf_drv_twi_tx(&IIS2DH_twi, IIS2DH_ADDRESS, wxbuffer_with_address, number_of_bytes+1, true);

//Wait for the transmission to be completed
while (IIS2DH_xfer_done == false){}

// If transmission was not successful, exit the function and return false
if (NRF_SUCCESS != err_code)
{
return false;
}
return true;

//Wait for the transmission to be completed
while (IIS2DH_xfer_done == false){}

return IIS2DH_xfer_ok;
}

bool setupTemp(){
Expand Down Expand Up @@ -187,17 +205,17 @@ bool setupAccelormeter(IIS2DH_OperatingModes mode, IIS2DH_DataRate rate, IIS2DH_
wx_reg1_buffer[0] = 0x07;
}
wx_reg1_buffer[0] = wx_reg1_buffer[0] | rate; // set the odr bits
/* BDU holds the OUT_* registers until both halves of a sample have been read,
* so a burst read can never pair the high byte of one sample with the low byte
* of the next. Without it the 400 Hz ODR tears samples against our frame rate. */
wx_reg4_buffer[0] = IIS2DH_CTRL_REG4_BDU | fs;
if (mode == IIS2DH_HighResolutionMode){
wx_reg4_buffer[0] = 0b00001000;
}else{
wx_reg4_buffer[0] = 0b00000000;
wx_reg4_buffer[0] |= IIS2DH_CTRL_REG4_HR;
}
wx_reg4_buffer[0] |= wx_reg4_buffer[0] | fs;



bool res = IIS2DH_register_write(IIS2DH_REG_CTRL_REG1, wx_reg1_buffer, 1);
res &= IIS2DH_register_write(IIS2DH_REG_CTRL_REG4, wx_reg4_buffer, 1);
return true;
return res;
}


Expand Down Expand Up @@ -246,6 +264,13 @@ bool getAccelerationData(uint16_t* X, uint16_t* Y, uint16_t* Z, IIS2DH_Operating

}

/* Destination of the accelerometer block for the frame currently being filled. */
static uint8_t * accel_block_ptr(void)
{
return &m_rx_buf[(NUMBER_OF_XFERS - 1) + buffer_counter * NUMBER_OF_XFERS]
.buffer[ACC_BLOCK_OFFSET];
}

static void finalizeCurrentFrame(void)
{
buffer_counter++;
Expand Down Expand Up @@ -279,36 +304,47 @@ void finalizeFrameWithoutIMU(void)

flag_add_IMU = false;

// Zero out the 6 bytes that would normally carry accel data
uint8_t *dst = &m_rx_buf[3 + buffer_counter * NUMBER_OF_XFERS].buffer[0] + 202 - 6;
dst[0] = 0;
dst[1] = 0;
dst[2] = 0;
dst[3] = 0;
dst[4] = 0;
dst[5] = 0;
/* Accelerometer streaming is off, so the tail of this frame holds real RF
* samples. Leave it alone: the host contract for meas_mode == 0 is a full
* ACQ_LENGTH_SAMPLES of ultrasound, not 397 samples plus three zeroed ones.
* (This used to be zeroed, which silently discarded the last three
* samples of every A-scan even with the IMU disabled.) */

finalizeCurrentFrame();
}

void getIIS2DHData2Buffer(){

IIS2DH_buffer_index = 0;
IIS2DH_register_read(IIS2DH_REG_OUT_X_H, &IIS2DH_buffer[IIS2DH_buffer_index++], 1);
IIS2DH_register_read(IIS2DH_REG_OUT_X_L, &IIS2DH_buffer[IIS2DH_buffer_index++], 1);
IIS2DH_register_read(IIS2DH_REG_OUT_Y_H, &IIS2DH_buffer[IIS2DH_buffer_index++], 1);
IIS2DH_register_read(IIS2DH_REG_OUT_Y_L, &IIS2DH_buffer[IIS2DH_buffer_index++], 1);
IIS2DH_register_read(IIS2DH_REG_OUT_Z_H, &IIS2DH_buffer[IIS2DH_buffer_index++], 1);
IIS2DH_register_read(IIS2DH_REG_OUT_Z_L, &IIS2DH_buffer[IIS2DH_buffer_index++], 1);
//NRF_LOG_INFO("Received: %u", IIS2DH_buffer_index);
/* One auto-incrementing burst read of OUT_X_L..OUT_Z_H replaces six separate
* single-register reads. Each of those cost an address write plus a data read,
* about 2.4 ms in total at 100 kHz, every millisecond of which blocked the main
* loop and so stalled the BLE drain. The burst is a single transaction: roughly
* 0.2 ms at 400 kHz. */
uint8_t axes[ACC_BLOCK_BYTES];

if (IIS2DH_register_read(IIS2DH_REG_OUT_X_L | IIS2DH_AUTO_INCREMENT,
axes, ACC_BLOCK_BYTES) == false)
{
// Sensor did not answer - send zeroes rather than a stale sample.
memset(axes, 0, sizeof(axes));
}

/* The burst returns low byte first per axis (X_L, X_H, Y_L, ...). The wire
* format is high byte first, which is what the host decoder expects, so swap
* each pair on the way into the frame. */
IIS2DH_buffer[0] = axes[1]; // X_H
IIS2DH_buffer[1] = axes[0]; // X_L
IIS2DH_buffer[2] = axes[3]; // Y_H
IIS2DH_buffer[3] = axes[2]; // Y_L
IIS2DH_buffer[4] = axes[5]; // Z_H
IIS2DH_buffer[5] = axes[4]; // Z_L
IIS2DH_buffer_index = ACC_BLOCK_BYTES;

while(flag_add_IMU == false) {}

flag_add_IMU = false;

memcpy(&m_rx_buf[3 + buffer_counter * NUMBER_OF_XFERS].buffer[0] + 202 - 6,
&IIS2DH_buffer[0],
6);
memcpy(accel_block_ptr(), &IIS2DH_buffer[0], ACC_BLOCK_BYTES);

finalizeCurrentFrame();
}
8 changes: 8 additions & 0 deletions fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/iis2dh.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@
#define IIS2DH_REG_CTRL_REG5 0x24
#define IIS2DH_REG_CTRL_REG6 0x25

/* Sub-address bit 7 makes the IIS2DH auto-increment the register pointer, so a
* whole block (e.g. OUT_X_L..OUT_Z_H) can be read in one I2C transaction. */
#define IIS2DH_AUTO_INCREMENT 0x80

/* CTRL_REG4 bits */
#define IIS2DH_CTRL_REG4_BDU 0x80 /* block data update: hold OUT_* until both halves read */
#define IIS2DH_CTRL_REG4_HR 0x08 /* high-resolution (12-bit) mode */

#define IIS2DH_REG_STATUS_REG_AUX 0x07
#define IIS2DH_REG_OUT_TEMP_L 0x0C
#define IIS2DH_REG_OUT_TEMP_H 0x0D
Expand Down
12 changes: 12 additions & 0 deletions fw/nrf52/ble_peripheral/US_probe_nRF52_firmware/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,18 @@ void in_pin_handler(nrf_drv_gpiote_pin_t pin, nrf_gpiote_polarity_t action)
if (pin == PIN_DATA_READY)
{
NRF_SPIM0->RXD.PTR = (uint32_t)&m_rx_buf[buffer_counter*NUMBER_OF_XFERS].buffer[0];

/* Start every frame from a known state.
*
* nrf_drv_timer_enable() only issues TASKS_START, and the matching
* nrf_drv_timer_disable() issues the deprecated TASKS_SHUTDOWN whose effect
* on the COUNTER register is not clearly specified. Clearing both timers
* here removes that dependency: the transfer counter is guaranteed to start
* this frame at zero, so the burst can never end early, and timer_timer
* always waits a full period before the first SPI START. */
nrf_drv_timer_clear(&timer_counter);
nrf_drv_timer_clear(&timer_timer);

// Enable timer and counter to start the four SPI transactions
nrf_drv_timer_enable(&timer_timer);
nrf_drv_timer_enable(&timer_counter);
Expand Down
Loading