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
1 change: 1 addition & 0 deletions bricks/_common/sources.mk
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ PBIO_SRC_C = $(addprefix lib/pbio/,\
drv/battery/battery_ev3.c \
drv/battery/battery_nxt.c \
drv/battery/battery_test.c \
drv/block_device/block_device_ev3.c \
drv/block_device/block_device_flash_stm32.c \
drv/block_device/block_device_test.c \
drv/block_device/block_device_w25qxx_stm32.c \
Expand Down
248 changes: 140 additions & 108 deletions lib/pbio/drv/adc/adc_ev3.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@
#include <stdint.h>
#include <string.h>

#include <contiki.h>

#include <pbdrv/adc.h>
#include <pbdrv/clock.h>
#include <pbdrv/gpio.h>

#include <pbio/error.h>
#include <pbio/os.h>
#include <pbio/util.h>

#include <tiam1808/spi.h>
Expand All @@ -25,15 +24,76 @@
#include <tiam1808/hw/hw_syscfg0_AM1808.h>
#include <tiam1808/armv5/am1808/interrupt.h>

#include "adc_ev3.h"
#include "../drv/block_device/block_device_ev3.h"
#include "../drv/gpio/gpio_ev3.h"

PROCESS(pbdrv_adc_process, "ADC");

#define PBDRV_CONFIG_ADC_EV3_NUM_DELAY_SAMPLES (4)

static volatile uint16_t channel_data[PBDRV_CONFIG_ADC_EV3_ADC_NUM_CHANNELS + PBDRV_CONFIG_ADC_EV3_NUM_DELAY_SAMPLES] = {0};
static volatile uint8_t channel_data_index = 0;
static volatile bool adc_busy = false;
#include "../sys/storage.h"

#define PBDRV_ADC_EV3_NUM_DELAY_SAMPLES (2)

/**
* Constants.
*/
enum {
// The maximum ADC clock speed according to the datasheet is 20 MHz.
// However, because the SPI peripheral does not have a fractional clock generator,
// the closest achievable in-spec speed is a division factor of 8.
//
// 150 MHz / 8 = 18.75 MHz actual
SPI_CLK_SPEED_ADC = 20000000,

ADC_SAMPLE_PERIOD = 10,
};

// Construct both SPI peripheral settings (data format, chip select)
// and ADC chip settings (manual mode, 2xVref) in one go,
// so that DMA can be used efficiently.
//
// NOTE: CSHOLD is *not* set here, so that CS is deasserted between each 16-bit unit
#define MANUAL_ADC_CHANNEL(x) \
(1 << 26) | \
(SPI_SPIDAT1_DFSEL_FORMAT1 << SPI_SPIDAT1_DFSEL_SHIFT) | \
(0 << (SPI_SPIDAT1_CSNR_SHIFT + PBDRV_EV3_SPI0_ADC_CS)) | \
(1 << (SPI_SPIDAT1_CSNR_SHIFT + PBDRV_EV3_SPI0_FLASH_CS)) | \
(1 << 12) | \
(1 << 11) | \
(((x) & 0xf) << 7) | \
(1 << 6)
Comment on lines +59 to +62
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have some macros or comments to explain these bits.


static const uint32_t channel_cmd[PBDRV_CONFIG_ADC_EV3_ADC_NUM_CHANNELS + PBDRV_ADC_EV3_NUM_DELAY_SAMPLES] = {
MANUAL_ADC_CHANNEL(0),
MANUAL_ADC_CHANNEL(1),
MANUAL_ADC_CHANNEL(2),
MANUAL_ADC_CHANNEL(3),
MANUAL_ADC_CHANNEL(4),
MANUAL_ADC_CHANNEL(5),
MANUAL_ADC_CHANNEL(6),
MANUAL_ADC_CHANNEL(7),
MANUAL_ADC_CHANNEL(8),
MANUAL_ADC_CHANNEL(9),
MANUAL_ADC_CHANNEL(10),
MANUAL_ADC_CHANNEL(11),
MANUAL_ADC_CHANNEL(12),
MANUAL_ADC_CHANNEL(13),
MANUAL_ADC_CHANNEL(14),
MANUAL_ADC_CHANNEL(15),
// We need two additional commands here because of how the ADC works.
// In every given command frame, a new analog channel is selected in the ADC frontend multiplexer.
// In frame n+1, that value actually gets converted to a digital value.
// In frame n+2, the converted digital value is finally output, and we are able to receive it.
// These requests are _pipelined_, so there is a latency of 2 frames, but we get a new sample on each frame.
//
// For more information, see figures 1 and 51 in the ADS7957 datasheet.
MANUAL_ADC_CHANNEL(15),
MANUAL_ADC_CHANNEL(15),
};
static volatile uint16_t channel_data[PBDRV_CONFIG_ADC_EV3_ADC_NUM_CHANNELS + PBDRV_ADC_EV3_NUM_DELAY_SAMPLES];

static int adc_soon;
// Used to block ADC from interfering with flash upon shutdown
static int shut_down_hack = 0;
static int shut_down_hack_done = 0;

static pbdrv_adc_callback_t pbdrv_adc_callbacks[1];
static uint32_t pbdrv_adc_callback_count = 0;
Expand All @@ -48,127 +108,99 @@ pbio_error_t pbdrv_adc_get_ch(uint8_t ch, uint16_t *value) {
if (ch >= PBDRV_CONFIG_ADC_EV3_ADC_NUM_CHANNELS) {
return PBIO_ERROR_INVALID_ARG;
}
// Values for the requested channel are received several samples later.
// The data only appears 12-bit but the last 2 bits are always zero.
*value = (channel_data[ch + PBDRV_CONFIG_ADC_EV3_NUM_DELAY_SAMPLES] - 4096 * ch) >> 2;
// XXX We probably need to figure out how atomicity works between the DMA and the CPU.
// For now, read the value twice and assume it's good (not torn) if the values are the same.
Comment on lines +111 to +112
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of worrying about tearing, I would do a double buffer. Just before we call pbdrv_adc_callbacks in the protothread where we drive the sampling, copy the DMA buffer to a 2nd buffer. We know we are not in the middle of a DMA operation at this point. Then have this function use the values from the 2nd buffer so that we know the data is good.

uint16_t a, b;
do {
// Values for the requested channel are received several samples later.
a = channel_data[ch + PBDRV_ADC_EV3_NUM_DELAY_SAMPLES];
b = channel_data[ch + PBDRV_ADC_EV3_NUM_DELAY_SAMPLES];
} while (a != b);

// Mask the data to 10 bits
*value = (a >> 2) & 0x3ff;
return PBIO_SUCCESS;
}

static void spi0_isr(void) {
uint32_t intCode = 0;
IntSystemStatusClear(SYS_INT_SPINT0);

while ((intCode = SPIInterruptVectorGet(SOC_SPI_0_REGS))) {
if (intCode != SPI_TX_BUF_EMPTY) {
continue;
}
// Payload encoding comes from the original EV3 sources, but we
// use the hardware SPI peripheral instead of bit-banging.
uint16_t payload = 0x1840 | (((channel_data_index % PBDRV_CONFIG_ADC_EV3_ADC_NUM_CHANNELS) & 0x000F) << 7);
HWREG(SOC_SPI_0_REGS + SPI_SPIDAT0) = payload;
channel_data[channel_data_index] = SPIDataReceive(SOC_SPI_0_REGS);

if (++channel_data_index == PBIO_ARRAY_SIZE(channel_data)) {
SPIIntDisable(SOC_SPI_0_REGS, SPI_TRANSMIT_INT);
adc_busy = false;
process_poll(&pbdrv_adc_process);
}
}
}

static void pbdrv_adc_exit(void) {
SPIIntDisable(SOC_SPI_0_REGS, SPI_RECV_INT | SPI_TRANSMIT_INT);
}
static pbio_os_process_t pbdrv_adc_ev3_process;

// ADC / Flash SPI0 data MOSI
static const pbdrv_gpio_t pin_spi0_mosi = PBDRV_GPIO_EV3_PIN(3, 15, 12, 8, 5);
pbio_error_t pbdrv_adc_ev3_process_thread(pbio_os_state_t *state, void *context) {
static pbio_os_timer_t timer;

// ADC / Flash SPI0 data MISO
static const pbdrv_gpio_t pin_spi0_miso = PBDRV_GPIO_EV3_PIN(3, 11, 8, 8, 6);
PBIO_OS_ASYNC_BEGIN(state);

// LCD SPI0 Clock
static const pbdrv_gpio_t pin_spi0_clk = PBDRV_GPIO_EV3_PIN(3, 3, 0, 1, 8);
// HACK: This waits until storage is completely done with SPI flash before we start
PBIO_OS_AWAIT_UNTIL(state, pbsys_storage_settings_get_settings());

// ADC / Flash SPI0 chip select (active low).
static const pbdrv_gpio_t pin_spi0_cs = PBDRV_GPIO_EV3_PIN(3, 27, 24, 8, 2);
// Once SPI flash init is finished, there is nothing further for us to do.
// We are ready to start sampling.

// ADCACK PIN
static const pbdrv_gpio_t pin_adc_ack = PBDRV_GPIO_EV3_PIN(19, 19, 16, 6, 2);
pbio_os_timer_set(&timer, ADC_SAMPLE_PERIOD);

// ADCBATEN
static const pbdrv_gpio_t pin_adc_bat_en = PBDRV_GPIO_EV3_PIN(1, 7, 4, 0, 6);

void pbdrv_adc_init(void) {

// Configure the GPIO pins.
pbdrv_gpio_alt(&pin_spi0_mosi, SYSCFG_PINMUX3_PINMUX3_15_12_SPI0_SIMO0);
pbdrv_gpio_alt(&pin_spi0_miso, SYSCFG_PINMUX3_PINMUX3_11_8_SPI0_SOMI0);
pbdrv_gpio_alt(&pin_spi0_clk, SYSCFG_PINMUX3_PINMUX3_3_0_SPI0_CLK);
pbdrv_gpio_alt(&pin_spi0_cs, SYSCFG_PINMUX3_PINMUX3_27_24_NSPI0_SCS3);

pbdrv_gpio_input(&pin_adc_ack);

pbdrv_gpio_out_high(&pin_adc_bat_en);
for (;;) {
PBIO_OS_AWAIT_UNTIL(state, shut_down_hack || adc_soon || pbio_os_timer_is_expired(&timer));

// Waking up the SPI1 instance.
PSCModuleControl(SOC_PSC_1_REGS, HW_PSC_SPI0, PSC_POWERDOMAIN_ALWAYS_ON, PSC_MDCTL_NEXT_ENABLE);
if (shut_down_hack) {
shut_down_hack_done = 1;
break;
}

// Register the ISR in the Interrupt Vector Table.
IntRegister(SYS_INT_SPINT0, spi0_isr);
IntChannelSet(SYS_INT_SPINT0, 2);
IntSystemEnable(SYS_INT_SPINT0);
if (adc_soon) {
adc_soon = 0;
pbio_os_timer_set(&timer, ADC_SAMPLE_PERIOD);
} else {
// TODO: There should probably be a pbio OS function for this
timer.start += timer.duration;
}

// Reset.
SPIReset(SOC_SPI_0_REGS);
SPIOutOfReset(SOC_SPI_0_REGS);
// Do a sample of all channels
pbdrv_block_device_ev3_spi_begin_for_adc(
channel_cmd,
channel_data,
PBDRV_CONFIG_ADC_EV3_ADC_NUM_CHANNELS + PBDRV_ADC_EV3_NUM_DELAY_SAMPLES);
PBIO_OS_AWAIT_WHILE(state, pbdrv_block_device_ev3_is_busy());

// Mode.
uint32_t spipc0 = SPI_SPIPC0_SOMIFUN | SPI_SPIPC0_SIMOFUN | SPI_SPIPC0_CLKFUN | SPI_SPIPC0_SCS0FUN3;
SPIModeConfigure(SOC_SPI_0_REGS, SPI_MASTER_MODE);
SPIPinControl(SOC_SPI_0_REGS, 0, 0, (unsigned int *)&spipc0);
for (uint32_t i = 0; i < pbdrv_adc_callback_count; i++) {
pbdrv_adc_callbacks[i]();
}
}

// Config.
SPIClkConfigure(SOC_SPI_0_REGS, SOC_SYSCLK_2_FREQ, 2000000, SPI_DATA_FORMAT0);
SPIConfigClkFormat(SOC_SPI_0_REGS, SPI_CLK_OUTOFPHASE | 0x00000010, SPI_DATA_FORMAT0);
SPIDelayConfigure(SOC_SPI_0_REGS, 10, 10, 10, 10);
SPIIntLevelSet(SOC_SPI_0_REGS, SPI_RECV_INTLVL | SPI_TRANSMIT_INTLVL);
SPIDefaultCSSet(SOC_SPI_0_REGS, 8);
PBIO_OS_ASYNC_END(PBIO_SUCCESS);
}

// Enable and loop around all channels.
SPIEnable(SOC_SPI_0_REGS);
void pbdrv_adc_init(void) {
// Immediately go into async mode so that we can wait for the SPI flash driver.
// We *don't* want to block the initial init phase, or else things will deadlock.

process_start(&pbdrv_adc_process);
pbio_os_process_start(&pbdrv_adc_ev3_process, pbdrv_adc_ev3_process_thread, NULL);
}

void pbdrv_adc_update_soon(void) {
process_poll(&pbdrv_adc_process);
adc_soon = 1;
pbio_os_request_poll();
}

PROCESS_THREAD(pbdrv_adc_process, ev, data) {
PROCESS_EXITHANDLER(pbdrv_adc_exit());

static struct etimer etimer;

PROCESS_BEGIN();

etimer_set(&etimer, 10);
for (;;) {
PROCESS_WAIT_EVENT_UNTIL((ev == PROCESS_EVENT_TIMER && etimer_expired(&etimer)) || ev == PROCESS_EVENT_POLL);

channel_data_index = 0;
adc_busy = true;
SPIEnable(SOC_SPI_0_REGS);
SPIIntEnable(SOC_SPI_0_REGS, SPI_TRANSMIT_INT);
PROCESS_WAIT_EVENT_UNTIL(ev == PROCESS_EVENT_POLL && !adc_busy);

for (uint32_t i = 0; i < pbdrv_adc_callback_count; i++) {
pbdrv_adc_callbacks[i]();
}

etimer_reset(&etimer);
}
void pbdrv_adc_ev3_configure_data_format() {
SPIClkConfigure(SOC_SPI_0_REGS, SOC_SYSCLK_2_FREQ, SPI_CLK_SPEED_ADC, SPI_DATA_FORMAT1);
// NOTE: Cannot be CPOL=1 CPHA=1 like SPI flash
// The ADC seems to use the last falling edge to trigger conversions (see Figure 1 in the datasheet).
SPIConfigClkFormat(SOC_SPI_0_REGS, SPI_CLK_POL_LOW | SPI_CLK_OUTOFPHASE, SPI_DATA_FORMAT1);
SPIShiftMsbFirst(SOC_SPI_0_REGS, SPI_DATA_FORMAT1);
SPICharLengthSet(SOC_SPI_0_REGS, 16, SPI_DATA_FORMAT1);
// In order to compensate for analog impedance issues and capacitor charging time,
// we set all SPI delays to the maximum for the ADC. This helps get more accurate readings.
// This includes both this delay (the delay where CS is held inactive),
// as well as the CS-assert-to-clock-start and clock-end-to-CS-deassert delays
// (which are global and set in block_device_ev3.c).
SPIWdelaySet(SOC_SPI_0_REGS, 0x3f << SPI_SPIFMT_WDELAY_SHIFT, SPI_DATA_FORMAT1);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also set other config explicitly here even if it is the same as flash.

For example, SPIDefaultCSSet() is an interesting one. Omitting SPI_CSHOLD there is important for using the ADC with DMA so that it toggles the CS line between each word.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which other config settings were you thinking of? SPIDefaultCSSet doesn't configure the CSHOLD bit. That bit is in fact cleared by the DMA commands to SPIDAT1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSHOLD was the main one. I guess I copied and pasted the wrong fucntion name. But it is nice to see what the full config is all in once place without relying on the flash memory driver setting up some of this for us. (And could save us some trouble if we ever tweak the flash memory driver without realizing it also affects the ADC).


PROCESS_END();
void pbdrv_adc_ev3_shut_down_hack() {
shut_down_hack = 1;
pbio_os_request_poll();
}
int pbdrv_adc_ev3_is_shut_down_hack() {
return shut_down_hack_done;
}

#endif // PBDRV_CONFIG_ADC_EV3
12 changes: 12 additions & 0 deletions lib/pbio/drv/adc/adc_ev3.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2025 The Pybricks Authors

#ifndef _INTERNAL_PBDRV_ADC_EV3_H_
#define _INTERNAL_PBDRV_ADC_EV3_H_

void pbdrv_adc_ev3_configure_data_format();

void pbdrv_adc_ev3_shut_down_hack();
int pbdrv_adc_ev3_is_shut_down_hack();

#endif // _INTERNAL_PBDRV_ADC_EV3_H_
Loading