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
29 changes: 29 additions & 0 deletions .github/workflows/arduino_zephyr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Zephyr

on:
push:
pull_request:
workflow_dispatch:

jobs:
build:
name: Compile open-loop velocity for Arduino UNO Q
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: arduino/setup-arduino-cli@v2

- name: Install Arduino Zephyr core
run: |
arduino-cli core update-index --additional-urls https://downloads.arduino.cc/packages/package_zephyr_index.json
arduino-cli core install arduino:zephyr --additional-urls https://downloads.arduino.cc/packages/package_zephyr_index.json

- name: Compile open-loop velocity example
run: |
mkdir -p "$RUNNER_TEMP/arduino-libraries"
ln -s "$GITHUB_WORKSPACE" "$RUNNER_TEMP/arduino-libraries/SimpleFOC"
arduino-cli compile \
--fqbn arduino:zephyr:unoq \
--libraries "$RUNNER_TEMP/arduino-libraries" \
examples/motion_control/open_loop_motor_control/open_loop_velocity_example
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SimpleFOC open-loop velocity — Arduino UNO Q (Zephyr core)
// Phases A/B/C on pins 8, 3, 6 -> all on TIM3 (TIM3_CH1 / CH3 / CH4)
// so the three PWM channels share one timer/time-base.
#include <SimpleFOC.h>

// BLDC motor & driver instance
BLDCMotor motor = BLDCMotor(11); // pole pairs
BLDCDriver3PWM driver = BLDCDriver3PWM(8, 3, 6, 7); // enable on D7

// target variable
float target_velocity = 0;

// commander interface
Commander command = Commander(Serial);
void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); }
void doLimit(char* cmd) { command.scalar(&motor.voltage_limit, cmd); }

void setup() {
Serial.begin(115200);
SimpleFOCDebug::enable(&Serial); // verbose output for debugging

// driver config
driver.voltage_power_supply = 12; // [V]
driver.voltage_limit = 5; // [V] hard cap the driver can output
driver.pwm_frequency = 25000; // [Hz] applied at runtime via pwm_set_pulse_dt()
if (!driver.init()) {
Serial.println("Driver init failed!");
return;
}
motor.linkDriver(&driver);

motor.foc_modulation = FOCModulationType::SpaceVectorPWM;

// motor limits
motor.voltage_limit = 3; // [V] start low for low-resistance motors

// open-loop velocity control
motor.controller = MotionControlType::velocity_openloop;

if (!motor.init()) {
Serial.println("Motor init failed!");
return;
}

// commands: T = target velocity [rad/s], L = voltage limit [V]
command.add('T', doTarget, "target velocity");
command.add('L', doLimit, "voltage limit");

Serial.println("Motor ready!");
Serial.println("Set target velocity [rad/s] with e.g. T5");
_delay(1000);
}

void loop() {
motor.loopFOC();
motor.move(target_velocity);
command.run();
}
79 changes: 79 additions & 0 deletions src/current_sense/hardware_specific/zephyr/zephyr_mcu.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@

#include "../../hardware_api.h"

#if defined(ARDUINO_ARCH_ZEPHYR)

#include <Arduino.h>
#include "../../../communication/SimpleFOCDebug.h"

// Inline current sensing on the Arduino Zephyr core (ArduinoCore-zephyr) uses the standard
// Arduino analogRead()/analogReadResolution() API, which reads the ADC channels declared in
// the `io-channels` property of the board's `zephyr,user` devicetree node.
//
// Low-side current sensing needs the ADC conversions to be triggered in sync with the PWM
// timer. That synchronization is not available through the Arduino wiring layer, so only
// inline current sensing is supported here.

// ADC read resolution used for current sensing. Overridable from the build environment.
#ifndef SIMPLEFOC_ZEPHYR_ADC_RESOLUTION
#define SIMPLEFOC_ZEPHYR_ADC_RESOLUTION 12
#endif

// ADC reference voltage in volts. Overridable from the build environment to match the
// board's analog reference.
#ifndef SIMPLEFOC_ZEPHYR_ADC_VOLTAGE
#define SIMPLEFOC_ZEPHYR_ADC_VOLTAGE 3.3f
#endif


// function reading an ADC value and returning the read voltage
float _readADCVoltageInline(const int pinA, const void* cs_params){
uint32_t raw_adc = analogRead(pinA);
return raw_adc * ((GenericCurrentSenseParams*)cs_params)->adc_voltage_conv;
}

// function configuring the ADC for inline current sensing
void* _configureADCInline(const void* driver_params, const int pinA, const int pinB, const int pinC){
_UNUSED(driver_params);

analogReadResolution(SIMPLEFOC_ZEPHYR_ADC_RESOLUTION);

if( _isset(pinA) ) pinMode(pinA, INPUT);
if( _isset(pinB) ) pinMode(pinB, INPUT);
if( _isset(pinC) ) pinMode(pinC, INPUT);

const float adc_range = (float)(1UL << SIMPLEFOC_ZEPHYR_ADC_RESOLUTION);

GenericCurrentSenseParams* params = new GenericCurrentSenseParams {
.pins = { pinA, pinB, pinC },
.adc_voltage_conv = (SIMPLEFOC_ZEPHYR_ADC_VOLTAGE) / adc_range
};

return params;
}

// low-side current sensing is not supported through the Arduino Zephyr wiring API
float _readADCVoltageLowSide(const int pinA, const void* cs_params){
_UNUSED(pinA);
_UNUSED(cs_params);
SIMPLEFOC_DEBUG("ERR: Low-side cs not supported on Zephyr core!");
return 0.0f;
}

void* _configureADCLowSide(const void* driver_params, const int pinA, const int pinB, const int pinC){
_UNUSED(driver_params);
_UNUSED(pinA);
_UNUSED(pinB);
_UNUSED(pinC);
SIMPLEFOC_DEBUG("ERR: Low-side cs not supported on Zephyr core!");
return SIMPLEFOC_CURRENT_SENSE_INIT_FAILED;
}

void* _driverSyncLowSide(void* driver_params, void* cs_params){
_UNUSED(driver_params);
return cs_params;
}

void _startADC3PinConversionLowSide(){ }

#endif
70 changes: 70 additions & 0 deletions src/drivers/hardware_specific/zephyr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Arduino Zephyr core support

Hardware-specific implementation for the [Arduino Zephyr core](https://github.com/arduino/ArduinoCore-zephyr)
(`ArduinoCore-zephyr`). It is selected automatically when the `ARDUINO_ARCH_ZEPHYR`
macro is defined by the build.

- Driver PWM: [`zephyr_mcu.cpp`](zephyr_mcu.cpp)
- Inline current sense: [`../../../current_sense/hardware_specific/zephyr/zephyr_mcu.cpp`](../../../current_sense/hardware_specific/zephyr/zephyr_mcu.cpp)

## What is supported

| Feature | Status | Notes |
|---|---|---|
| 1-PWM / 2-PWM / 3-PWM / 4-PWM | ✅ | Zephyr PWM timers, runtime frequency control |
| 6-PWM (complementary) | ❌ | needs hardware dead-time; not portable via `pwm_set_dt()` |
| Inline current sensing | ✅ | via `analogRead()` |
| Low-side current sensing | ❌ | needs ADC/PWM synchronization; not exposed by the wiring API |

## How it works

The PWM is generated by driving the **Zephyr PWM timers directly** through the native
`pwm_set_pulse_dt()` API — the same approach used by the
[Arduino_HardwareServo](https://github.com/arduino-libraries/Arduino_HardwareServo) library.
This gives full **runtime control of the PWM frequency**: the requested `pwm_frequency`
(Hz) is converted to a period in nanoseconds and stored once, at configuration time, into a
per-pin copy of the channel's `pwm_dt_spec`. Each duty-cycle update then only pushes the new
pulse width with `pwm_set_pulse_dt()`, reusing that stored period.

The PWM channels are taken from the `pwms` property of the board's `zephyr,user` devicetree
node and mapped to Arduino pin numbers through the `pwm-pin-gpios` property, exactly as the
Arduino Zephyr core does internally.

If a requested pin is **not** mapped to a PWM channel in the devicetree, the driver falls
back to the wiring `analogWrite()` (which degrades to a digital HIGH/LOW output) and prints
a warning — so make sure your driver pins are declared under `pwms` / `pwm-pin-gpios`.

Current sensing uses the standard `analogRead()` / `analogReadResolution()` API, reading the
ADC channels declared in `io-channels`.

## Board devicetree overlay (important)

Your **board overlay** must declare which pins can output PWM and which can be read by the
ADC. The PWM frequency itself is set at runtime by SimpleFOC via `pwm_set_dt()`, but each
`pwms` entry still needs a (nonzero) default `period`.

Example `zephyr,user` overlay fragment:

```dts
/ {
zephyr,user {
pwms = <&pwm0 0 PWM_HZ(25000) PWM_POLARITY_NORMAL>,
<&pwm0 1 PWM_HZ(25000) PWM_POLARITY_NORMAL>,
<&pwm0 2 PWM_HZ(25000) PWM_POLARITY_NORMAL>;
pwm-pin-gpios = <&gpio0 2 0>, <&gpio0 3 0>, <&gpio0 4 0>;

io-channels = <&adc 0>, <&adc 1>, <&adc 2>;
adc-pin-gpios = <&gpio0 5 0>, <&gpio0 6 0>, <&gpio0 7 0>;
};
};
```

## Build-time options

Override from your build environment / `build_flags` if needed:

| Macro | Default | Meaning |
|---|---|---|
| `SIMPLEFOC_ZEPHYR_PWM_RESOLUTION` | `12` | `analogWrite` resolution (bits), fallback pins only |
| `SIMPLEFOC_ZEPHYR_ADC_RESOLUTION` | `12` | `analogRead` resolution (bits) |
| `SIMPLEFOC_ZEPHYR_ADC_VOLTAGE` | `3.3f` | ADC reference voltage (V) for current sensing |
Loading
Loading