diff --git a/.github/workflows/build_examples.yaml b/.github/workflows/build_examples.yaml index d554f88..d66bf69 100644 --- a/.github/workflows/build_examples.yaml +++ b/.github/workflows/build_examples.yaml @@ -23,8 +23,8 @@ jobs: - name: Generate build matrix id: gen_matrix run: | - # Get all subdirectories containing platformio.ini in the ./example directory - subdirectories=$(find ./examples -type f -name "platformio.ini" -exec dirname {} \; | sort -u) + # Get all subdirectories containing platformio.ini in the ./examples and ./libraries/*/examples directories + subdirectories=$(find ./examples ./libraries/*/examples -type f -name "platformio.ini" -exec dirname {} \; | sort -u) # Convert subdirectories to JSON array json_array="[" diff --git a/cores/arduino/drivers/interrupts/interrupts.cpp b/cores/arduino/drivers/interrupts/interrupts.cpp index d71fa82..32ee6e8 100644 --- a/cores/arduino/drivers/interrupts/interrupts.cpp +++ b/cores/arduino/drivers/interrupts/interrupts.cpp @@ -269,7 +269,7 @@ en_result_t _irqn_aa_resign(IRQn_Type &irqn) { // do nothing since resigning the interrupt already frees the IRQn // only check that the interrupt was actually resigned before calling this function - CORE_ASSERT(ram_vector_table.irqs[irqn] != no_handler, "IRQ was not resigned before auto-assign resignment", + CORE_ASSERT(ram_vector_table.irqs[irqn] == no_handler, "IRQ was not resigned before auto-assign resignment", return Error); return Ok; } diff --git a/libraries/SPI/src/SPI.cpp b/libraries/SPI/src/SPI.cpp index e322e23..a7b414d 100644 --- a/libraries/SPI/src/SPI.cpp +++ b/libraries/SPI/src/SPI.cpp @@ -3,6 +3,8 @@ #include #include +#warning "SPI on the HC32F460 has not been tested yet. See https://github.com/shadow578/framework-arduino-hc32f46x/pull/29" + /** * @brief given a integer v, round up to the next power of two * @note based on https://stackoverflow.com/a/466242 diff --git a/libraries/SPI/src/SPI.h b/libraries/SPI/src/SPI.h index 6886934..e51a0a1 100644 --- a/libraries/SPI/src/SPI.h +++ b/libraries/SPI/src/SPI.h @@ -14,8 +14,6 @@ #error "SPI library requires PWC DDL to be enabled" #endif -#warning "SPI on the HC32F460 has not been tested yet. See https://github.com/shadow578/framework-arduino-hc32f46x/pull/29" - // SPI_HAS_TRANSACTION means SPI has // - beginTransaction() diff --git a/libraries/SoftwareSerial/README.md b/libraries/SoftwareSerial/README.md new file mode 100644 index 0000000..08def0b --- /dev/null +++ b/libraries/SoftwareSerial/README.md @@ -0,0 +1,92 @@ +# Software Serial for the HC32F460 + +The Software Serial library allows serial (UART) communication on any digital pin of the board, bit-banging the protocol. +It it possible to have multiple software serial ports. + +The implementation of this library is based on the [SoftwareSerial library of the STM32duino project](https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/SoftwareSerial/). + + +## Configuration Options + +To configure the library, you may add the following defines to your build environment. + +| Name | Default | Description | +|-|-|-| +| `SOFTWARE_SERIAL_BUFFER_SIZE` | `32` | size of the receive buffer. it's highly likely that any transmission longer than this will be partially lost. | +| `SOFTWARE_SERIAL_OVERSAMPLE` | `3` | oversampling rate. Each bit period is equal to OVERSAMPLE ticks, and bits are sampled in the middle | +| `SOFTWARE_SERIAL_HALF_DUPLEX_SWITCH_DELAY` | `5` | bit periods before half duplex switches TX to RX | +| `SOFTWARE_SERIAL_TIMER_PRESCALER` | `2` | prescaler of the TIMER0. set according to PCLK1 and desired baud rate range | +| `SOFTWARE_SERIAL_TIMER0_UNIT` | `TIMER01B_config` | TIMER0 unit to use for software serial. Using TIMER01A is not recommended | +| `SOFTWARE_SERIAL_TIMER_PRIORITY` | `3` | interrupt priority of the timer interrupt | +| `SOFTWARE_SERIAL_FLUSH_CLEARS_RX_BUFFER` | `SOFTWARE_SERIAL_STM32_API_COMPATIBILITY` | behaviour of the `flush()` method. `0` = waits for pending TX to complete. `1` = clear RX buffer. STMduino library uses behaviour `1` | +| `SOFTWARE_SERIAL_STM32_API_COMPATIBILITY` | `0` | compatibility with STM32duino library. `0` = sensible API. `1` = compatible with STM32duino API. | + + +> [!TIP] +> for existing projects that originated from STM32duino, you may set `SOFTWARE_SERIAL_STM32_API_COMPATIBILITY` to `1` to maintain compatibility with the STM32duino library. + + +### Calculating `SOFTWARE_SERIAL_TIMER_PRESCALER` + +to calculate, use the following c++ program + +```cpp +#include +#include + +float get_real_frequency(const uint32_t frequency, const uint16_t prescaler) +{ + const uint32_t base_frequency = 50000000; // 50 MHz PCLK1 + + // calculate the compare value needed to match the target frequency + // CMP = (base_freq / prescaler) / frequency + uint32_t compare = (base_frequency / uint32_t(prescaler)) / frequency; + + if (compare <= 0 || compare > 0xFFFF) + { + return -1; + } + + // calculate the real frequency + float real_frequency = (base_frequency / prescaler) / compare; + return real_frequency; +} + + +int main() +{ + const uint32_t baud = 9600; + const uint32_t oversampling = 3; + + const uint32_t frequency = baud * oversampling; + const uint16_t prescalers[] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024}; + + float min_error = 100000; + uint16_t best_prescaler = 0; + for (auto p : prescalers) + { + const float real_frequency = get_real_frequency(frequency, p); + const float error = std::abs(real_frequency - frequency); + if (error < min_error) + { + min_error = error; + best_prescaler = p; + } + + std::cout << "Prescaler: " << static_cast(p) + << ", Real frequency: " << real_frequency + << ", Error: " << error + << std::endl; + } + + float best_real_frequency = get_real_frequency(frequency, best_prescaler); + float best_baud = best_real_frequency / oversampling; + float baud_percent_error = (best_baud - baud) / baud * 100; + std::cout << "Best prescaler: " << static_cast(best_prescaler) + << ", Real frequency: " << best_real_frequency + << ", Error: " << min_error + << ", Real baud rate: " << best_baud + << ", Baud rate error: " << baud_percent_error << "%" + << std::endl; +} +``` diff --git a/libraries/SoftwareSerial/examples/SoftwareSerialExample/.gitignore b/libraries/SoftwareSerial/examples/SoftwareSerialExample/.gitignore new file mode 100644 index 0000000..b9f3806 --- /dev/null +++ b/libraries/SoftwareSerial/examples/SoftwareSerialExample/.gitignore @@ -0,0 +1,2 @@ +.pio +.vscode diff --git a/libraries/SoftwareSerial/examples/SoftwareSerialExample/platformio.ini b/libraries/SoftwareSerial/examples/SoftwareSerialExample/platformio.ini new file mode 100644 index 0000000..83ec8fa --- /dev/null +++ b/libraries/SoftwareSerial/examples/SoftwareSerialExample/platformio.ini @@ -0,0 +1,17 @@ +[env] +platform = https://github.com/shadow578/platform-hc32f46x/archive/1.1.0.zip +framework = arduino +board = generic_hc32f460 +build_flags = + -D USART_AUTO_CLKDIV_OS_CONFIG # enable auto clock division and oversampling configuration (recommended) + -D __DEBUG=1 + -D __CORE_DEBUG=1 + +[env:default] + +# required only for CI +[env:ci] +# override the framework-arduino-hc32f46x package with the local one +board_build.arduino_package_dir = ../../../../ +extra_scripts = + pre:../../../../tools/ci/patch_get_package_dir.py diff --git a/libraries/SoftwareSerial/examples/SoftwareSerialExample/src/main.cpp b/libraries/SoftwareSerial/examples/SoftwareSerialExample/src/main.cpp new file mode 100644 index 0000000..b77328c --- /dev/null +++ b/libraries/SoftwareSerial/examples/SoftwareSerialExample/src/main.cpp @@ -0,0 +1,54 @@ +/* + * Software serial basic example. + * + * echos any character received on the software serial port back to the sender. + * also allows to change the baud rate by sending a number from 1 to 8. + * + * without changing the system clock, the maximum baud rate is around 9600 baud. + */ +#include +#include + +constexpr gpio_pin_t RX_PIN = PA15; +constexpr gpio_pin_t TX_PIN = PA9; + +SoftwareSerial mySerial(/* RX */ RX_PIN, /* TX */ TX_PIN); + +void setup() +{ + mySerial.begin(9600); + mySerial.println("Hello, world!"); +} + +void loop() +{ + while (mySerial.available()) + { + const char c = mySerial.read(); + mySerial.write(c); + + uint32_t new_baud = 0; + switch(c) + { + case '1': new_baud = 1200; break; + case '2': new_baud = 2400; break; + case '3': new_baud = 4800; break; + case '4': new_baud = 9600; break; + case '5': new_baud = 19200; break; + case '6': new_baud = 38400; break; + case '7': new_baud = 57600; break; + case '8': new_baud = 115200; break; + default: break; + } + if (new_baud != 0) + { + mySerial.print("Set baud to:"); + mySerial.print(new_baud); + delay(100); + mySerial.begin(new_baud); + mySerial.println(" - Done"); + } + } + + delay(10); +} diff --git a/libraries/SoftwareSerial/src/SoftwareSerial.cpp b/libraries/SoftwareSerial/src/SoftwareSerial.cpp new file mode 100644 index 0000000..8fc0998 --- /dev/null +++ b/libraries/SoftwareSerial/src/SoftwareSerial.cpp @@ -0,0 +1,519 @@ +#include "SoftwareSerial.h" +#include + +#warning "SoftwareSerial on HC32F460 is experimental!" + +static_assert(SOFTWARE_SERIAL_BUFFER_SIZE > 0, "SOFTWARE_SERIAL_BUFFER_SIZE must be > 0"); +static_assert(SOFTWARE_SERIAL_OVERSAMPLE >= 3, "SOFTWARE_SERIAL_OVERSAMPLE must be >= 3"); +static_assert(SOFTWARE_SERIAL_HALF_DUPLEX_SWITCH_DELAY >= 0, "SOFTWARE_SERIAL_HALF_DUPLEX_SWITCH_DELAY must be >= 0"); + +#ifdef __CORE_DEBUG +/*static*/ uint8_t SoftwareSerial::next_id = 0; + +#define SOFTSERIAL_DEBUG_PRINTF(fmt, ...) \ + CORE_DEBUG_PRINTF("[SoftwareSerial#%u] " fmt, this->id, ##__VA_ARGS__) + +#define SOFTSERIAL_STATIC_DEBUG_PRINTF(fmt, ...) \ + CORE_DEBUG_PRINTF("[SoftwareSerial] " fmt, ##__VA_ARGS__) +#else +#define SOFTSERIAL_DEBUG_PRINTF(fmt, ...) +#define SOFTSERIAL_STATIC_DEBUG_PRINTF(fmt, ...) +#endif + +SoftwareSerial::SoftwareSerial(const gpio_pin_t rx_pin, const gpio_pin_t tx_pin, const bool invert) + : + #ifdef __CORE_DEBUG + id(next_id++), + #endif + rx_pin(rx_pin), tx_pin(tx_pin), invert(invert) +{ + this->rx_buffer = new RingBuffer(SOFTWARE_SERIAL_BUFFER_SIZE); + CORE_ASSERT(this->rx_buffer != nullptr, ""); +} + +SoftwareSerial::~SoftwareSerial() +{ + end(); + + #if SOFTWARE_SERIAL_STM32_API_COMPATIBILITY == 1 + remove_listener(this); + #endif + + delete this->rx_buffer; +} + +void SoftwareSerial::begin(const uint32_t baud) +{ + // if already started once, end first + if (this->baud != 0) + { + end(); + #if SOFTWARE_SERIAL_STM32_API_COMPATIBILITY == 1 + remove_listener(this); + #endif + } + + SOFTSERIAL_DEBUG_PRINTF("begin: rx=%u, tx=%u, invert=%d, baud=%lu, half-duplex=%d\n", + rx_pin, + tx_pin, + invert, + baud, + is_half_duplex()); + + this->baud = baud; + + // half-duplex starts out in TX mode, so it is always enabled + setup_tx(); + if (!is_half_duplex()) + { + setup_rx(); + listen(); + } + + // make the timer ISR call this instance + add_listener(this); +} + +void SoftwareSerial::end() +{ + SOFTSERIAL_DEBUG_PRINTF("end\n"); + stopListening(); + + #if SOFTWARE_SERIAL_STM32_API_COMPATIBILITY == 0 + remove_listener(this); + this->baud = 0; + #endif +} + +bool SoftwareSerial::listen() +{ + rx_wait_ticks = 1; // next interrupt will check for start bit + rx_bit_count = -1; // wait for start bit + + // change the speed of the timer + // this function automatically waits for all pending TX operations to finish + const bool did_speed_change = timer_set_speed(baud); + + // enable RX + if (is_half_duplex()) + { + set_half_duplex_mode(true /*=RX*/); + } + else + { + rx_active = true; + } + + SOFTSERIAL_DEBUG_PRINTF("started listening @baud=%lu; did_speed_change=%d\n", baud, did_speed_change); + return did_speed_change; +} + +bool SoftwareSerial::isListening() +{ + return current_timer_speed == baud; +} + +bool SoftwareSerial::stopListening() +{ + // wait for any pending TX operations to finish + while (tx_active) + yield(); + + // disable RX + const bool was_listening = rx_active || tx_active; + if (is_half_duplex()) + { + set_half_duplex_mode(false /*=TX*/); + } + else + { + rx_active = false; + } + + #if SOFTWARE_SERIAL_STM32_API_COMPATIBILITY == 0 + // if no other instance is listening, stop the timer + bool any_listening = false; + ListenerItem *item = listeners; + while (item != nullptr) + { + // don't check this instance, we're already stopping ;) + if (item->listener != this && item->listener->isListening()) + { + any_listening = true; + break; + } + + item = item->next; + } + + if (!any_listening) + { + timer_set_speed(0); + } + #endif + + SOFTSERIAL_DEBUG_PRINTF("stopped listening; was_listening=%d\n", was_listening); + return was_listening; +} + +bool SoftwareSerial::overflow() +{ + const bool overflow = did_rx_overflow; + did_rx_overflow = false; + return overflow; +} + +int SoftwareSerial::peek() +{ + return rx_buffer->peek(); +} + +size_t SoftwareSerial::write(const uint8_t byte) +{ + // in case this is half-duplex, setting tx_pending will avert the switch to RX mode + tx_pending = true; + + // wait for previous TX to finish + while (tx_active) + yield(); + + // add start and stop bits + tx_frame = (byte << 1) | 0x200; + if (invert) + { + tx_frame = ~tx_frame; + } + + // ensure timer is running at the correct speed + timer_set_speed(baud); + + // ensure TX is enabled in half-duplex mode + // this call is a no-op if not in half-duplex mode, so no additional check + set_half_duplex_mode(false /*=TX*/); + + // start transmission on next interrupt + tx_bit_count = 0; + tx_wait_ticks = 1; + + tx_pending = false; + tx_active = true; + return 1; +} + +int SoftwareSerial::read() +{ + uint8_t e; + if (rx_buffer->pop(e)) + { + return e; + } + + return -1; +} + +int SoftwareSerial::available() +{ + return rx_buffer->count(); +} + +void SoftwareSerial::flush() +{ +#if SOFTWARE_SERIAL_FLUSH_CLEARS_RX_BUFFER == 1 + // clear RX buffer + rx_buffer->clear(); +#else + // wait for any pending TX operations to finish + while (tx_active) + yield(); +#endif +} + +void SoftwareSerial::setup_rx() +{ + // note: cannot call DEBUG_PRINTF here, this may be called from a ISR + // SOFTSERIAL_DEBUG_PRINTF("setup_rx on %u\n", rx_pin); + + // UART idle line is high, so set pull-up for non-inverted logic + // HC32 has no pull-down, so inverted logic will have to do without + pinMode(rx_pin, invert ? INPUT : INPUT_PULLUP); +} + +void SoftwareSerial::setup_tx() +{ + // note: cannot call DEBUG_PRINTF here, this may be called from a ISR + // SOFTSERIAL_DEBUG_PRINTF("setup_tx on %u\n", tx_pin); + + // set pin level before setting as output to avoid glitches + // UART idle line is high, so set high for non-inverted logic and vice versa + if (invert) GPIO_ResetBits(tx_pin); + else GPIO_SetBits(tx_pin); + + pinMode(tx_pin, OUTPUT); +} + +void SoftwareSerial::set_half_duplex_mode(const bool rx) +{ + // if not half-duplex mode, ignore this + if (!is_half_duplex()) return; + + if (rx) + { + tx_active = false; + setup_rx(); + + rx_bit_count = -1; // waiting for start bit + rx_wait_ticks = 2; // wait 2 bit times for start bit + rx_active = true; + } + else + { + if (rx_active) + { + rx_active = false; + setup_tx(); + } + } +} + +// +// ISR +// + +void SoftwareSerial::do_rx() +{ + // if not enabled, do nothing + if (!rx_active) return; + + // if tick count is non-zero, continue waiting + rx_wait_ticks--; + if (rx_wait_ticks > 0) return; + + // read bit, invert if inverted logic + const bool bit = GPIO_GetBit(rx_pin) ^ invert; + + // waiting for start bit? + if (rx_bit_count == -1) + { + // is start bit? + // this is UART, so idle line is high and start bit is going low + if (!bit) + { + rx_frame = 0; + rx_bit_count = 0; + + // wait 1 1/2 bit times to sample in the middle of the bit + rx_wait_ticks = SOFTWARE_SERIAL_OVERSAMPLE + (SOFTWARE_SERIAL_OVERSAMPLE >> 1); + } + else + { + // waiting for start bit, but didn't get it + // wait for next interrupt to check again + rx_wait_ticks = 1; + } + } + else if (rx_bit_count >= 8) // waiting for stop bit? + { + // is stop bit? + // this is UART, so stop bit (== idle line) is high + if (bit) + { + // add byte to buffer + bool overflow; + rx_buffer->push(rx_frame, true, overflow); + + // avoid overwriting overflow flag + if (overflow) did_rx_overflow = true; + } + + // assume frame is completed, wait for next start bit at next interrupt + // even if there was no stop bit + rx_bit_count = -1; + rx_wait_ticks = 1; + } + else // data bits + { + rx_frame >>= 1; + if (bit) rx_frame |= 0x80; + rx_bit_count++; + rx_wait_ticks = SOFTWARE_SERIAL_OVERSAMPLE; + } + +} + +void SoftwareSerial::do_tx() +{ + // if not enabled, do nothing + if (!tx_active) return; + + // if tick count is non-zero, continue waiting + tx_wait_ticks--; + if (tx_wait_ticks > 0) return; + + // all bits in frame sent? + if (tx_bit_count >= 10) + { + // if no frame is pending and half-duplex, switch to RX mode + // otherwise, we're done transmitting + if (tx_pending || !is_half_duplex()) + { + tx_active = false; + } + else if (is_half_duplex() && isListening()) + { + // wait HALF_DUPLEX_SWITCH_DELAY bit periods before switching to RX mode + // note: due to the tx_wait_ticks=1 in this branch, tx_bit_count is incremented every tick and not every bit period + // thus, to get bit periods, we need to multiply the delay by OVERSAMPLE + if (tx_bit_count >= 10 + (SOFTWARE_SERIAL_HALF_DUPLEX_SWITCH_DELAY * SOFTWARE_SERIAL_OVERSAMPLE)) + { + set_half_duplex_mode(true /*=RX*/); + } + + // keep incrementing bit count to time the above check + tx_bit_count++; + } + + tx_wait_ticks = 1; + return; + } + + // send next bit + if (tx_frame & 1) GPIO_SetBits(tx_pin); + else GPIO_ResetBits(tx_pin); + + tx_frame >>= 1; + tx_bit_count++; + tx_wait_ticks = SOFTWARE_SERIAL_OVERSAMPLE; +} + +// +// Timer control +// + +/*static*/ uint32_t SoftwareSerial::current_timer_speed = 0; +/*static*/ Timer0 SoftwareSerial::timer(&SOFTWARE_SERIAL_TIMER0_UNIT, SoftwareSerial::timer_isr); +/*static*/ SoftwareSerial::ListenerItem *SoftwareSerial::listeners = nullptr; + +/*static*/ bool SoftwareSerial::timer_set_speed(const uint32_t baud) +{ + if (current_timer_speed == baud) return false; + + // stop timer? + if (baud == 0) + { + SOFTSERIAL_STATIC_DEBUG_PRINTF("timer_set_speed stopping timer\n"); + timer.pause(); + timer.stop(); + current_timer_speed = 0; + return true; + } + + // speed change operations are fairly costly because they block until pending TX operations finish + // so print a warning if this happens + if (current_timer_speed != 0) + { + SOFTSERIAL_STATIC_DEBUG_PRINTF("baud rate change from %lu to %lu. Consider configuring all your software serials to the same baud rate to improve performance.\n", + current_timer_speed, + baud); + + // wait for all pending TX operations in active channels to finish before changing speed + ListenerItem *item = listeners; + while (item != nullptr) + { + while (item->listener->tx_active) + yield(); + + item = item->next; + } + } + + // (re-) initialize timer to the baud rate frequency, with oversampling + // if already running, timer will automatically stop in the start() call + timer.start(baud * SOFTWARE_SERIAL_OVERSAMPLE, SOFTWARE_SERIAL_TIMER_PRESCALER); + + // set priority if initial start + if (current_timer_speed == 0) + { + setInterruptPriority(SOFTWARE_SERIAL_TIMER_PRIORITY); + } + current_timer_speed = baud; + + #ifdef __CORE_DEBUG + const float actual_baud = (timer.get_actual_frequency() / SOFTWARE_SERIAL_OVERSAMPLE); + SOFTSERIAL_STATIC_DEBUG_PRINTF("timer_set_speed target baud=%lu; actual baud=%d.%d\n", + baud, + static_cast(actual_baud), + static_cast((actual_baud - static_cast(actual_baud)) * 100) + ); + #endif + + timer.resume(); // needed to actually start the timer + return true; + +} + +/*static*/ void SoftwareSerial::add_listener(SoftwareSerial *listener) +{ + // pause timer while modifying listener list to avoid race conditions + timer.pause(); + + ListenerItem *item = new ListenerItem; + CORE_ASSERT(item != nullptr, ""); + + item->listener = listener; + item->next = listeners; + listeners = item; + + timer.resume(); +} + +/*static*/ void SoftwareSerial::remove_listener(SoftwareSerial *listener) +{ + ListenerItem *prev = nullptr; + ListenerItem *item = listeners; + while (item != nullptr) + { + if (item->listener == listener) + { + // pause timer while modifying listener list to avoid race conditions + timer.pause(); + + if (prev == nullptr) + { + listeners = item->next; + } + else + { + prev->next = item->next; + } + + timer.resume(); + + delete item; + return; + } + + prev = item; + item = item->next; + } +} + +/*static*/ void SoftwareSerial::timer_isr() +{ + ListenerItem *item = listeners; + while (item != nullptr) + { + // only call RX/TX if instance uses the correct baud rate + if (item->listener->isListening()) + { + item->listener->do_tx(); + item->listener->do_rx(); + } + + item = item->next; + } +} + +/*static*/ void SoftwareSerial::setInterruptPriority(const uint32_t priority) +{ + timer.setCallbackPriority(priority); +} diff --git a/libraries/SoftwareSerial/src/SoftwareSerial.h b/libraries/SoftwareSerial/src/SoftwareSerial.h new file mode 100644 index 0000000..a033802 --- /dev/null +++ b/libraries/SoftwareSerial/src/SoftwareSerial.h @@ -0,0 +1,255 @@ +#ifndef SOFTWARESERIAL_H +#define SOFTWARESERIAL_H + +#include +#include +#include + +#ifndef SOFTWARE_SERIAL_BUFFER_SIZE +#define SOFTWARE_SERIAL_BUFFER_SIZE 32 +#endif + +#ifndef SOFTWARE_SERIAL_OVERSAMPLE +#define SOFTWARE_SERIAL_OVERSAMPLE 3 +#endif + +#ifndef SOFTWARE_SERIAL_HALF_DUPLEX_SWITCH_DELAY +#define SOFTWARE_SERIAL_HALF_DUPLEX_SWITCH_DELAY 5 // bit-periods +#endif + +#ifndef SOFTWARE_SERIAL_TIMER_PRESCALER +#define SOFTWARE_SERIAL_TIMER_PRESCALER 2 +#endif + +// recommented to not use TIMER0 Unit 1 Channel A, as it does not support sync mode +#ifndef SOFTWARE_SERIAL_TIMER0_UNIT +#define SOFTWARE_SERIAL_TIMER0_UNIT TIMER01B_config // Timer0 Unit 1, Channel B +#endif + +#ifndef SOFTWARE_SERIAL_TIMER_PRIORITY +#define SOFTWARE_SERIAL_TIMER_PRIORITY 3 +#endif + +// changes the way the SoftwareSerial library behaves to match the one from STM32duino. +// this is useful for compatibility with existing code, however these changes cause the +// behaviour of the API to be less intuitive. +// main changes: +// 1. flush() will clear the RX buffer instead of waiting for all pending TX operations to finish +// 2. end() will no longer fully undo begin(). +// you'll be able to write and receive even after calling end() +// 3. stopListening() will not stop the timer if it is no longer needed. +// resulting from this, the timer is never stopped. + +#ifndef SOFTWARE_SERIAL_STM32_API_COMPATIBILITY +#define SOFTWARE_SERIAL_STM32_API_COMPATIBILITY 0 +#endif + +// how SoftwareSerial behaves when flush() is called +// when 0: flush() will wait for all pending TX operations to finish (Arduinio >1.0 behavior) +// when 1: flush() will clear the RX buffer (old behaviour; how the STM32duino library does it) +#ifndef SOFTWARE_SERIAL_FLUSH_CLEARS_RX_BUFFER +#define SOFTWARE_SERIAL_FLUSH_CLEARS_RX_BUFFER SOFTWARE_SERIAL_STM32_API_COMPATIBILITY +#endif + +/** + * Software Serial implementation using Timer0. + * loosely based on STM32duino SoftwareSerial library. + * see https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/SoftwareSerial/ + * + * @note + * This SoftwareSerial implementation has some caveats due to technical limitations: + * a) While you may define as many software serial instances as you want, there can + * only ever be one active baud rate at a time. + * This means that two instances at the same baud rate can run at the same time, + * but if you write to a software serial instance with a different baud rate, + * the other instances will stop sending and receiving data until you call listen() on one of them. + * b) Switching the baud rate is fairly slow, because it waits for all pending TX operations to finish. + * Additionally, a baud rate switch may cause data loss on the RX side. + * Due to this, it is recommended to configure all software serial instances to the same baud rate. + * c) The timer prescaler must be manually chosen to match the desired baud rate range. + * Set SOFTWARE_SERIAL_TIMER_PRESCALER such that the frequency error is minimal. + * The default value of 2 is good for PCLK1=50MHz and OVERSAMPLE=3 for baud rates + * between 1200 to 38400 baud, with < 0.01% error. + * For baud rates between 38400 and 115200, a prescaler of 1 is recommended (< 0.5% error), + * but a prescaler of 2 does also work. + * d) On the default clock rate of the arduino core (8MHz), the maximum baud rate is 9600, with a prescaler of 1. + */ +class SoftwareSerial : public Stream +{ +#ifdef __CORE_DEBUG +private: + static uint8_t next_id; + const uint8_t id; +#endif + +public: + /** + * @brief create a SoftwareSerial instance + * @param rx_pin receive pin + * @param tx_pin transmit pin + * @param invert invert high and low on RX and TX lines + * @note when rx_pin == tx_pin, half-duplex mode is enabled + */ + SoftwareSerial(const gpio_pin_t rx_pin, const gpio_pin_t tx_pin, const bool invert = false); + virtual ~SoftwareSerial(); + + /** + * @brief setup the software serial + * @param baud baud rate + */ + void begin(const uint32_t baud); + + /** + * @brief de-initialize the software serial + */ + void end(); + + /** + * @brief start listening for incoming data + * @returns true if a speed change occurred. + * If this is the case, serials using a different baud rate will + * stop sending and receiving data util listen() is called on them. + */ + bool listen(); + + /** + * @brief check if this software serial is listening + * @note + * multiple software serials can be listening at the same time, + * as long as they are using the same baud rate. + */ + bool isListening(); + + /** + * @brief stop listening for incoming data + * @returns true if this software serial was previously listening + */ + bool stopListening(); + + bool overflow(); + + int peek(); + + virtual size_t write(const uint8_t byte); + virtual int read(); + virtual int available(); + virtual void flush(); + + operator bool() + { + return true; + } + + using Print::write; + +private: // common + const gpio_pin_t rx_pin; + const gpio_pin_t tx_pin; + const bool invert; + uint32_t baud = 0; + + inline bool is_half_duplex() + { + return rx_pin == tx_pin; + } + + /** + * @brief setup RX pin for receiving + */ + void setup_rx(); + + /** + * @brief setup TX pin for transmitting + */ + void setup_tx(); + + /** + * @brief setup RX and TX pins for half-duplex communication + * @param rx true for RX, false for TX + * @note no-op if not in half-duplex mode + */ + void set_half_duplex_mode(const bool rx); + +private: // RX logic + RingBuffer *rx_buffer; + bool did_rx_overflow = false; + bool rx_active = false; + + uint8_t rx_frame = 0; // 8 bits + int8_t rx_bit_count = -1; // -1 means waiting for start bit + int8_t rx_wait_ticks = 0; + + /** + * @brief receive a single bit. called by the timer ISR + */ + void do_rx(); + +private: // TX logic + bool tx_active = false; + bool tx_pending = false; + + uint16_t tx_frame = 0; // 10 bits + int8_t tx_bit_count = 0; + int8_t tx_wait_ticks = 0; + + /** + * @brief transmit a single bit. called by the timer ISR + */ + void do_tx(); + +private: // Timer0 ISR logic + /** + * @brief baud rate that software serial is running at (ALL of them). + * @note 0 if not initialized + */ + static uint32_t current_timer_speed; + + /** + * @brief Timer0 instance + */ + static Timer0 timer; + + /** + * @brief set the timer baud rate + * @param baud baud rate to set the timer to. 0 to stop the timer + * @return true if a speed change occurred + */ + static bool timer_set_speed(const uint32_t baud); + + struct ListenerItem + { + SoftwareSerial *listener; + ListenerItem *next; + }; + + /** + * @brief list of software serials that should be called in the timer ISR + */ + static ListenerItem *listeners; + + /** + * @brief add a listener to the timer ISR + * @param listener software serial instance to add + */ + static void add_listener(SoftwareSerial *listener); + + /** + * @brief remove a listener from the timer ISR + * @param listener software serial instance to remove + */ + static void remove_listener(SoftwareSerial *listener); + + /** + * @brief timer callback + */ + static void timer_isr(); + +public: + /** + * @brief set the interrupt priority for the SoftwareSerial timer + * @param priority interrupt priority to set + */ + static void setInterruptPriority(const uint32_t priority); +}; + +#endif // SOFTWARESERIAL_H diff --git a/libraries/Timer0/src/Timer0.cpp b/libraries/Timer0/src/Timer0.cpp index 43af4b8..a5e2e9c 100644 --- a/libraries/Timer0/src/Timer0.cpp +++ b/libraries/Timer0/src/Timer0.cpp @@ -63,6 +63,42 @@ inline en_tim0_clock_div_t numeric_to_clock_div(const uint16_t n) } } +/** + * @brief convert en_tim0_clock_div_t to numerical value + * @note assert fails if invalid value + */ +inline uint16_t clock_div_to_numeric(const en_tim0_clock_div_t div) +{ + switch (div) + { + case Tim0_ClkDiv0: + return 1; + case Tim0_ClkDiv2: + return 2; + case Tim0_ClkDiv4: + return 4; + case Tim0_ClkDiv8: + return 8; + case Tim0_ClkDiv16: + return 16; + case Tim0_ClkDiv32: + return 32; + case Tim0_ClkDiv64: + return 64; + case Tim0_ClkDiv128: + return 128; + case Tim0_ClkDiv256: + return 256; + case Tim0_ClkDiv512: + return 512; + case Tim0_ClkDiv1024: + return 1024; + default: + CORE_ASSERT_FAIL("Invalid clock divider value"); + return 1; + } +} + /** * @brief timer0 interrupt registration */ @@ -224,3 +260,37 @@ void Timer0::stop() TIMER0_DEBUG_PRINTF("stopped channel\n"); } + +float Timer0::get_actual_frequency() +{ + // get timer channel base frequency, refer to start() for details + uint32_t base_frequency; + if (this->config->peripheral.register_base == M4_TMR01 && this->config->interrupt.interrupt_source == INT_TMR01_GCMA) + { + base_frequency = LRC_VALUE; + } + else + { + update_system_clock_frequencies(); + base_frequency = SYSTEM_CLOCK_FREQUENCIES.pclk1; + } + + // get prescaler from peripheral registers + en_tim0_clock_div_t prescaler_reg; + if (this->config->peripheral.channel == Tim0_ChannelA) + { + prescaler_reg = static_cast(this->config->peripheral.register_base->BCONR_f.CKDIVA); + } + else + { + prescaler_reg = static_cast(this->config->peripheral.register_base->BCONR_f.CKDIVB); + } + + const uint16_t prescaler = clock_div_to_numeric(prescaler_reg); + + // get compare value from peripheral registers + const uint16_t compare = TIMER0_GetCmpReg(this->config->peripheral.register_base, this->config->peripheral.channel); + + // calculate actual frequency + return (static_cast(base_frequency) / static_cast(prescaler)) / static_cast(compare); +} diff --git a/libraries/Timer0/src/Timer0.h b/libraries/Timer0/src/Timer0.h index 5b5bff6..ba4c3a4 100644 --- a/libraries/Timer0/src/Timer0.h +++ b/libraries/Timer0/src/Timer0.h @@ -174,6 +174,14 @@ class Timer0 TIMER0_ClearFlag(this->config->peripheral.register_base, this->config->peripheral.channel); } + /** + * @brief get the actual frequency of the timer0 channel + * @return actual frequency of the timer0 channel + * @note calculates the frequency from the live register values, so this + * is what the timer is currently running at. + */ + float get_actual_frequency(); + private: timer0_channel_config_t *config; voidFuncPtr callback; diff --git a/tools/platformio/platformio-build-arduino.py b/tools/platformio/platformio-build-arduino.py index 2ccbf7d..f39d5ae 100644 --- a/tools/platformio/platformio-build-arduino.py +++ b/tools/platformio/platformio-build-arduino.py @@ -124,7 +124,8 @@ def get_version_defines() -> list[str]: "spi", "sram", "timera", - "usart", + "timer0", + "usart" ] for req in core_requirements: board.update(f"build.ddl.{req}", "true")