diff --git a/CHANGELOG.md b/CHANGELOG.md index 309e0c6c..8ea6009a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -206,6 +206,11 @@ * Separate ./README on parts for firmware dev and contributor parts * Corrected library.json – Fix a license type from MIT to Apache * TODO Rework bits module +* PCA/CCP module implemented (pca.h): pin group selection, counter clock source, 16 bit counter, compare/capture, high speed toggle output and 8/7/6 bit PWM +* PCA examples added to examples/pca (PWM, 16 bit timer and capture) +* Host tests for library headers added to test/ and wired to ctest +* SPI module implemented (spi.h): master and slave mode, 4 clock dividers, all 4 CPOL/CPHA modes, MSB/LSB first, 3 pin groups, sync and async transfer, write collision detection +* SPI example added to examples/spi (master loopback) Next releases todo diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 1d477678..00000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,37 +0,0 @@ -cmake_minimum_required(VERSION 3.31) # Specify minimum CMake version - -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") - -file(REAL_PATH "cmake/toolchain-SDCC.cmake" CMAKE_TOOLCHAIN_FILE EXPAND_TILDE) - -project( - stc15hal - VERSION 0.17.0 - LANGUAGES C ASM -) # Define project name and language - -set(CMAKE_C_STANDARD 23) -set(CMAKE_C_STANDARD_REQUIRED ON) - -# add headers to stc15hal -include_directories( - ${CMAKE_SOURCE_DIR}/include # For public headers during build -) - -add_subdirectory("src") -add_subdirectory("examples") -add_subdirectory("utils") - -find_package(Doxygen REQUIRED dot) -if(Doxygen_FOUND) - message(STATUS "Found Doxygen Version " - "${DOXYGEN_VERSION} at ${DOXYGEN_EXECUTABLE}") - - add_custom_target(docs ALL - COMMAND doxygen ${CMAKE_CURRENT_SOURCE_DIR}/doxygen.conf - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMENT "Generating project documentation" - ) -else() - message(STATUS "Doxygen not found. Docs generation disabled") -endif() \ No newline at end of file diff --git a/README.md b/README.md index 0d999538..aa5366d3 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ void sleep_ms(uint16_t ms) { | **EEPROM / IAP** | `eeprom.h` | Read byte, write byte/array, and sector erase via IAP registers | Ready | | **ADC** | `adc.h` | 10-bit analog-to-digital converter (sync blocking and async interrupt modes) | Ready | | **Comparator** | `comparator.h` | On-chip analog voltage comparator | Initial Support | +| **PCA / CCP** | `pca.h` | 16-bit counter, compare/capture, high-speed toggle output, 8/7/6-bit PWM | Initial Support | +| **SPI** | `spi.h` | Master & slave, 3 pin groups, 4 clock dividers, CPOL/CPHA modes, sync & async | Initial Support | | **Bit Operations** | `bits.h` | Fast bit set, clear, toggle, and test macros | Ready | ### Code Examples @@ -316,9 +318,12 @@ STC15/ │ ├── eeprom/ # EEPROM read, write byte/page, erase examples │ ├── gpio/ # GPIO pin modes and values examples │ ├── interrupt/ # External interrupt & priority examples +│ ├── pca/ # PCA/CCP: PWM, 16-bit timer, and capture examples │ ├── power_management/# Idle, Power-Down, and Wake-up timer examples +│ ├── spi/ # SPI master & slave examples │ ├── timer/ # Timer0 & Timer2 mode examples (sync & async) │ └── uart/ # UART1 modes 0, 1, 2, 3 examples +├── test/ # Host tests for the library headers (ctest) ├── utils/ # Diagnostic and data conversion tools (mem_dump, csv2hex) ├── docs/ # Generated Doxygen documentation ├── doxygen.conf # Doxygen documentation configuration diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index d647789b..97c2187e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -8,8 +8,10 @@ add_subdirectory("eeprom") add_subdirectory("frequency") add_subdirectory("interrupt") add_subdirectory("gpio") +add_subdirectory("pca") add_subdirectory("power_management") add_subdirectory("reset") +add_subdirectory("spi") add_subdirectory("timer") add_subdirectory("uart") add_subdirectory("wdt") diff --git a/examples/SConstruct b/examples/SConstruct index 896d1db8..9799c4f2 100644 --- a/examples/SConstruct +++ b/examples/SConstruct @@ -23,8 +23,10 @@ eeprom_progs = SConscript('eeprom/SConscript', exports = 'env', variant_dir= frequency_progs = SConscript('frequency/SConscript', exports = 'env', variant_dir= base_dir + '/frequency') gpio_progs = SConscript('gpio/SConscript', exports = 'env', variant_dir= base_dir + '/gpio') interrupt_progs = SConscript('interrupt/SConscript', exports = 'env', variant_dir= base_dir + '/interrupt') +pca_progs = SConscript('pca/SConscript', exports = 'env', variant_dir= base_dir + '/pca') pwr_mgmt_progs = SConscript('power_management/SConscript', exports = 'env', variant_dir= base_dir + '/power_management') reset_progs = SConscript('reset/SConscript', exports = 'env', variant_dir= base_dir + '/reset') +spi_progs = SConscript('spi/SConscript', exports = 'env', variant_dir= base_dir + '/spi') timer_progs = SConscript('timer/SConscript', exports = 'env', variant_dir= base_dir + '/timer') uart_progs = SConscript('uart/SConscript', exports = 'env', variant_dir= base_dir + '/uart') wdt_progs = SConscript('wdt/SConscript', exports = 'env', variant_dir= base_dir + '/wdt') @@ -41,8 +43,10 @@ Return([ 'frequency_progs', 'gpio_progs', 'interrupt_progs', + 'pca_progs', 'pwr_mgmt_progs', 'reset_progs', + 'spi_progs', 'timer_progs', 'uart_progs', 'wdt_progs', diff --git a/examples/pca/CMakeLists.txt b/examples/pca/CMakeLists.txt new file mode 100644 index 00000000..33f84d1b --- /dev/null +++ b/examples/pca/CMakeLists.txt @@ -0,0 +1,40 @@ +add_executable(pca_pwm_example + pca_pwm_example.c +) +add_executable(pca_timer_example + pca_timer_example.c +) +add_executable(pca_capture_example + pca_capture_example.c +) + +target_link_libraries(pca_pwm_example PRIVATE stc15hal) +target_link_libraries(pca_timer_example PRIVATE stc15hal) +target_link_libraries(pca_capture_example PRIVATE stc15hal) + +# Transform firmware ihx to hex +ihx_to_hex(pca_pwm_example) +ihx_to_hex(pca_timer_example) +ihx_to_hex(pca_capture_example) + +# upload firmware to STC MCU +add_custom_target(flash_pca_pwm + COMMAND stcgal ${CMAKE_BINARY_DIR}/examples/pca/pca_pwm_example.hex + DEPENDS pca_pwm_example + COMMENT "Waiting for MCU, please cycle power:" + USES_TERMINAL +) +# upload firmware to STC MCU +add_custom_target(flash_pca_timer + COMMAND stcgal ${CMAKE_BINARY_DIR}/examples/pca/pca_timer_example.hex + DEPENDS pca_timer_example + COMMENT "Waiting for MCU, please cycle power:" + USES_TERMINAL +) +# upload firmware to STC MCU +add_custom_target(flash_pca_capture + COMMAND stcgal ${CMAKE_BINARY_DIR}/examples/pca/pca_capture_example.hex + DEPENDS pca_capture_example + COMMENT "Waiting for MCU, please cycle power:" + USES_TERMINAL +) diff --git a/examples/pca/SConscript b/examples/pca/SConscript new file mode 100644 index 00000000..7e498f4f --- /dev/null +++ b/examples/pca/SConscript @@ -0,0 +1,28 @@ +Import("env") + +pca_pwm_prog = env.Program( + 'pca_pwm_example', + ['pca_pwm_example.c'] +) + +pca_timer_prog = env.Program( + 'pca_timer_example', + ['pca_timer_example.c'] +) + +pca_capture_prog = env.Program( + 'pca_capture_example', + ['pca_capture_example.c'] +) + +env.Default( + pca_pwm_prog, + pca_timer_prog, + pca_capture_prog +) + +Return([ + 'pca_pwm_prog', + 'pca_timer_prog', + 'pca_capture_prog', +]) diff --git a/examples/pca/pca_capture_example.c b/examples/pca/pca_capture_example.c new file mode 100644 index 00000000..6b558211 --- /dev/null +++ b/examples/pca/pca_capture_example.c @@ -0,0 +1,77 @@ +/** + * How to measure a pulse width with PCA module 0 in 16 bit capture mode. + * + * The PCA counter is clocked from SYSclk/12, so one counter tick is 12 + * system clocks. Module 0 captures the counter value on both edges of the + * CCP0 pin, and the difference between two captures is the pulse width in + * counter ticks. + * + * Pin group 1 is used, so CCP0 is P1.1. The pin is an input here and must + * be configured as input only. + * + * In capture mode CAPPn and/or CAPNn are set and ECOMn is left clear. + * Setting both CAPP0 and CAPN0 captures on every transition, which makes + * the handler measure the high time and the low time alternately. + */ + +#include +#include +#include + +// CCP0 is P1.1 in pin group 1. The port is passed to the gpio macros as the +// literal P1 because they paste it into a register name (port ## M1). +#define CCP0_PIN 1 + +// Captured counter value of the previous edge +static uint16_t previous_capture = 0; + +// Width of the last pulse in PCA counter ticks +static uint16_t pulse_width = 0; + +/* + * PCA interrupt handler. + * + * The CCF0 flag is cleared before the capture registers are read, so a new + * capture cannot be missed while the value is being copied out. + */ +void pca_ISR(void) __interrupt(INTERRUPT_PCA) +{ + if (is_pca_module_flag_set(0)) + { + pca_clear_module_flag(0); + + uint16_t capture = pca_module_get_capture(0); + + // Unsigned arithmetic handles the counter wrap around + pulse_width = capture - previous_capture; + previous_capture = capture; + } +} + +void main(void) +{ + // CCP0 is P1.1 in pin group 1 and is used as an input + pca_set_pin_group(PCA_PIN_GROUP_1); + pin_input_only_init(P1, CCP0_PIN); + + // Counter clock is SYSclk/12 + pca_set_clock_source(PCA_CLOCK_SYS_12); + + // Module 0 captures on both edges of CCP0, ECOMn stays clear + pca_module_enable_capture_positive(0); + pca_module_enable_capture_negative(0); + + // CCF0 generates a PCA interrupt + pca_module_enable_interrupt(0); + + // Load the counter and start it + pca_set_counter(0); + pca_start(); + + enable_mcu_interrupts(); + + while (1) + { + // pulse_width holds the width of the last pulse in counter ticks + } +} diff --git a/examples/pca/pca_pwm_example.c b/examples/pca/pca_pwm_example.c new file mode 100644 index 00000000..d31b3da8 --- /dev/null +++ b/examples/pca/pca_pwm_example.c @@ -0,0 +1,53 @@ +/** + * How to generate a PWM signal on the CCP0 pin with PCA module 0. + * + * The PCA counter is clocked from SYSclk/12 and module 0 is used as an + * 8 bit PWM. The duty cycle is changed every 100 ms, so a LED connected + * to the CCP0 pin fades up and down. + * + * Pin group 1 is used, so CCP0 is P1.1. The pin must be configured as + * push-pull output, otherwise it cannot drive a LED. + * + * PWM frequency is SYSclk / (12 * 256) for 8 bit resolution. With the + * default 24 MHz clock this is about 7.8 kHz. + */ + +#include +#include +#include + +// CCP0 is P1.1 in pin group 1. The port is passed to the gpio macros as the +// literal P1 because they paste it into a register name (port ## M1). +#define CCP0_PIN 1 + +#define DUTY_STEP 1 + +void main(void) +{ + uint8_t duty = 0; + + // CCP0 is P1.1 in pin group 1, must be push-pull to drive a LED + pca_set_pin_group(PCA_PIN_GROUP_1); + pin_push_pull_init(P1, CCP0_PIN); + + // Counter clock is SYSclk/12, module 0 is an 8 bit PWM + pca_set_clock_source(PCA_CLOCK_SYS_12); + pca_module_pwm_init(0, PCA_PWM_8BIT); + + // Start with a zero duty cycle + pca_module_pwm_set_duty(0, 0); + + // Load the counter and start it + pca_set_counter(0); + pca_start(); + + while (1) + { + // CCAP0H is a reload register, the new duty value is applied on + // the next counter overflow without a glitch on the output + pca_module_pwm_set_duty_on_overflow(0, duty); + + duty += DUTY_STEP; + delay_ms(100); + } +} diff --git a/examples/pca/pca_timer_example.c b/examples/pca/pca_timer_example.c new file mode 100644 index 00000000..11f4b3ae --- /dev/null +++ b/examples/pca/pca_timer_example.c @@ -0,0 +1,75 @@ +/** + * How to use PCA module 0 as a 16 bit software timer. + * + * The PCA counter is clocked from SYSclk/12. Module 0 compare value is + * set to 10000, so the CCF0 flag is set every 10000 counter ticks. With + * the default 24 MHz clock this is every 5 ms. + * + * The module interrupt is enabled and the CCF0 flag is cleared in the + * handler, so the ISR runs every 5 ms. A LED on P1.0 is toggled every + * 100 interrupts, which is every 500 ms. + * + * Note that the PCA counter overflow flag CF and the module flags CCF0, + * CCF1 and CCF2 share the single PCA interrupt vector. + */ + +#include +#include + +// LED pin +#define LED_PIN P10 + +// Compare value, CCF0 is set every 10000 counter ticks +#define COMPARE_VALUE 10000 + +// Number of interrupts in 500 ms +#define INTERRUPTS_PER_HALF_SECOND 100 + +/* + * PCA interrupt handler. + * + * The CCF0 flag is set on module 0 compare match and must be cleared by + * software. The counter overflow flag CF is not used here, but if it were + * enabled it would have to be cleared as well. + */ +void pca_ISR(void) __interrupt(INTERRUPT_PCA) +{ + static uint8_t counter = 0; + + if (is_pca_module_flag_set(0)) + { + pca_clear_module_flag(0); + + counter++; + if (counter >= INTERRUPTS_PER_HALF_SECOND) + { + counter = 0; + LED_PIN = !LED_PIN; + } + } +} + +void main(void) +{ + // Counter clock is SYSclk/12 + pca_set_clock_source(PCA_CLOCK_SYS_12); + + // Module 0 compares the counter with COMPARE_VALUE + pca_module_set_compare(0, COMPARE_VALUE); + pca_module_enable_comparator(0); + pca_module_enable_match(0); + + // CCF0 generates a PCA interrupt + pca_module_enable_interrupt(0); + + // Load the counter and start it + pca_set_counter(0); + pca_start(); + + enable_mcu_interrupts(); + + while (1) + { + // All the work is done in pca_ISR + } +} diff --git a/examples/spi/CMakeLists.txt b/examples/spi/CMakeLists.txt new file mode 100644 index 00000000..aae465d1 --- /dev/null +++ b/examples/spi/CMakeLists.txt @@ -0,0 +1,16 @@ +add_executable(spi_master_example + spi_master_example.c +) + +target_link_libraries(spi_master_example PRIVATE stc15hal) + +# Transform firmware ihx to hex +ihx_to_hex(spi_master_example) + +# upload firmware to STC MCU +add_custom_target(flash_spi_master + COMMAND stcgal ${CMAKE_BINARY_DIR}/examples/spi/spi_master_example.hex + DEPENDS spi_master_example + COMMENT "Waiting for MCU, please cycle power:" + USES_TERMINAL +) diff --git a/examples/spi/SConscript b/examples/spi/SConscript new file mode 100644 index 00000000..17fd3ff8 --- /dev/null +++ b/examples/spi/SConscript @@ -0,0 +1,14 @@ +Import("env") + +spi_master_prog = env.Program( + 'spi_master_example', + ['spi_master_example.c'], +) + +env.Default( + spi_master_prog +) + +Return([ + 'spi_master_prog' +]) diff --git a/examples/spi/spi_master_example.c b/examples/spi/spi_master_example.c new file mode 100644 index 00000000..a3801f98 --- /dev/null +++ b/examples/spi/spi_master_example.c @@ -0,0 +1,37 @@ +/** How to transfer data over SPI in master mode + * + * Connect the MISO (P1.4) pin to the MOSI (P1.3) pin, so every byte sent is + * received back. The received byte should be equal to the sent one. + */ + +#include + +#include +#include +#include + +void main(void) +{ + uint8_t tx = 0; + uint8_t rx; + + uart1_init(9600); + + // Init SPI as a master on pin group 1 (SS P1.2, MOSI P1.3, MISO P1.4, + // SCLK P1.5), mode 0, SYSclk/16, MSB first + spi_init_master(SPI_PIN_GROUP_1, SPI_MODE_0, SPI_CLOCK_DIV_16, false); + + while (1) + { + // In mode 0 the SS pin is not ignored, so the slave has to be + // selected before every byte and deselected after it + spi_ss_clr(SPI_PIN_GROUP_1); + spi_transfer_sync(&rx, tx); + spi_ss_set(SPI_PIN_GROUP_1); + + printf_tiny("Sent %x, got %x\r\n", tx, rx); + + tx++; + delay_ms(1000); + } +} diff --git a/include/pca.h b/include/pca.h new file mode 100644 index 00000000..87e28110 --- /dev/null +++ b/include/pca.h @@ -0,0 +1,627 @@ +#ifndef STC15_PCAH +#define STC15_PCAH + +/** + * @file pca.h + * + * @defgroup pca PCA/CCP + * + * @details Functions and data structures related to PCA module + * + * PCA - Programmable Counter Array. It is a 16 bit counter (CH/CL) with + * three independent compare/capture modules (module 0, 1 and 2). Each module + * can be configured to work in one of the following modes: + * + * - Capture on positive and/or negative edge on the CEXn pin + * - 16 bit software timer (compare) with optional interrupt + * - High speed output: toggle CEXn pin on compare match + * - Pulse width modulator (PWM) output on the CEXn pin + * + * PCA counter clock source is selected with pca_set_clock_source() and could be + * SYSclk with different dividers, Timer0 overflow or external ECI pin. + * + * PCA counter overflow (CF flag) and module compare/capture events (CCFn flags) + * share the single PCA interrupt vector. Use INTERRUPT_PCA from interrupt.h for + * the interrupt handler. + * + * Pin mapping is selected with pca_set_pin_group(). Three pin groups are + * available and the group selection is stored in P_SW1 register: + * + * | Group | ECI | CCP0 | CCP1 | CCP2 | + * |-------|-------|-------|-------|-------| + * | 1 | P1.2 | P1.1 | P1.0 | P3.7 | + * | 2 | P3.4 | P3.5 | P3.6 | P3.7 | + * | 3 | P2.4 | P2.5 | P2.6 | P2.7 | + * + * PWM output resolution is 8, 7 or 6 bit depending on the module PCA_PWMn + * register EBSn bits. The datasheet also describes a software technique + * (section 11.8) which achieves 9..16 bit PWM by combining hardware compare + * with software updates, it is not implemented by this module. + * + * Note: only the STC15W401AS series (which includes STC15W408AS) peripherals + * are implemented here. The enhanced PWM waveform generator registers + * (PWMCFG/PWMCR/PWMCKS) defined in STC15Fxx.h belong to the STC15W4K32S4 + * series and are not usable on STC15W401AS devices. + * + * @author Michael Golovanov + */ + +#include +#include +#include +#include + +/** @brief CMOD register CPS0 (counter pulse select) bit position */ +#define PCA_CPS0_BIT 1 +/** @brief CMOD register CPS (counter pulse select) bits mask */ +#define PCA_CPS_MSK 0x0E +/** @brief PCA_PWMn register EBS0 (PWM resolution select) bit position */ +#define PCA_PWM_EBS0_BIT 6 +/** @brief PCA_PWMn register EBS (PWM resolution select) bits mask */ +#define PCA_PWM_EBS_MSK 0xC0 + +/** @brief P_SW1 register CCP_S0 (CCP pin group select) bit position */ +#define PCA_CCP_S0_BIT 4 +/** @brief P_SW1 register CCP_S0 (CCP pin group select) bit mask */ +#define PCA_CCP_S0_MSK 0x10 +/** @brief P_SW1 register CCP_S1 (CCP pin group select) bit mask */ +#define PCA_CCP_S1_MSK 0x20 +/** @brief P_SW1 register CCP pin group select bits mask */ +#define PCA_CCP_SW_MSK (uint8_t)(~(PCA_CCP_S0_MSK | PCA_CCP_S1_MSK)) + +/** + * @brief PCA pin group + * + * @details describes possible CCP/ECI pin mapping variants. + * Group selection is done with pca_set_pin_group(). + * + * @ingroup pca + */ +typedef enum +{ + /** CCP on [P1.2/ECI, P1.1/CCP0, P1.0/CCP1, P3.7/CCP2] */ + PCA_PIN_GROUP_1 = 0, + /** CCP on [P3.4/ECI_2, P3.5/CCP0_2, P3.6/CCP1_2, P3.7/CCP2_2] */ + PCA_PIN_GROUP_2 = 1, + /** CCP on [P2.4/ECI_3, P2.5/CCP0_3, P2.6/CCP1_3, P2.7/CCP2_3] */ + PCA_PIN_GROUP_3 = 2 +} pca_pin_group_t; + +/** + * @brief PCA counter clock source + * + * @details describes possible PCA counter pulse select variants. + * Clock source selection is done with pca_set_clock_source(). + * + * @ingroup pca + */ +typedef enum +{ + /** System clock, SYSclk/12 */ + PCA_CLOCK_SYS_12 = 0, + /** System clock, SYSclk/2 */ + PCA_CLOCK_SYS_2 = 1, + /** Timer0 overflow pulse */ + PCA_CLOCK_TIMER0 = 2, + /** External clock at ECI pin, max frequency SYSclk/2 */ + PCA_CLOCK_ECI = 3, + /** System clock, SYSclk */ + PCA_CLOCK_SYS = 4, + /** System clock, SYSclk/4 */ + PCA_CLOCK_SYS_4 = 5, + /** System clock, SYSclk/6 */ + PCA_CLOCK_SYS_6 = 6, + /** System clock, SYSclk/8 */ + PCA_CLOCK_SYS_8 = 7 +} pca_clock_source_t; + +/** + * @brief PCA module PWM output resolution + * + * @details describes possible PWM resolutions. Resolution is set with + * pca_module_pwm_init(). + * + * Note that the hardware supports only 8, 7 and 6 bit resolutions. The + * datasheet section 11.8 describes a software technique for 9..16 bit PWM + * which is not implemented by this module. + * + * @ingroup pca + */ +typedef enum +{ + /** Module works as 8 bit PWM */ + PCA_PWM_8BIT = 0, + /** Module works as 7 bit PWM */ + PCA_PWM_7BIT = 1, + /** Module works as 6 bit PWM */ + PCA_PWM_6BIT = 2 +} pca_pwm_resolution_t; + + +/** @name pin group + * PCA pin mapping functions + */ +///@{ + +/** + * @brief Set CCP/ECI pin group + * @details Select which pins are used as ECI, CCP0, CCP1 and CCP2. + * Pins are selected with P_SW1 register CCP_S1 and CCP_S0 bits. + * + * By default (after MCU power on) pin group 1 is used. + * + * @param group pca_pin_group_t pin group to use + * + * @ingroup pca + */ +#define pca_set_pin_group(group) \ +do { \ + bit_clr(P_SW1, PCA_CCP_SW_MSK); \ + bit_set(P_SW1, (uint8_t)((group) << PCA_CCP_S0_BIT)); \ +} while(0) + +/** + * @brief Get CCP/ECI pin group + * + * @return pca_pin_group_t current pin group + * + * @ingroup pca + */ +#define pca_get_pin_group() ((pca_pin_group_t)(bit_shift_right(get_reg(P_SW1, (PCA_CCP_S0_MSK | PCA_CCP_S1_MSK)), PCA_CCP_S0_BIT))) + +///@} + +/** @name counter + * PCA counter functions + */ +///@{ + +/** + * @brief Set PCA counter clock source + * @details Select PCA counter pulse source. Clock source is selected with + * CMOD register CPS2, CPS1 and CPS0 bits. Other CMOD bits are not changed. + * + * @param src pca_clock_source_t clock source + * + * @ingroup pca + */ +#define pca_set_clock_source(src) \ +do { \ + bit_clr(CMOD, (uint8_t)(~PCA_CPS_MSK)); \ + bit_set(CMOD, (uint8_t)((src) << PCA_CPS0_BIT)); \ +} while(0) + +/** + * @brief Get PCA counter clock source + * + * @return pca_clock_source_t current clock source + * + * @ingroup pca + */ +#define pca_get_clock_source() ((pca_clock_source_t)(bit_shift_right(get_reg(CMOD, PCA_CPS_MSK), PCA_CPS0_BIT))) + +/** + * @brief Set PCA counter value + * @details Load 16 bit value into PCA counter CH/CL registers. + * + * @param value uint16_t counter value + * + * @ingroup pca + */ +#define pca_set_counter(value) \ +do { \ + CH = (uint8_t)bit_shift_right((uint16_t)(value), 8); \ + CL = (uint8_t)(value); \ +} while(0) + +/** + * @brief Get PCA counter value + * + * @return uint16_t PCA counter value + * + * @ingroup pca + */ +#define pca_get_counter() ((uint16_t)(bit_shift_left((uint16_t)CH, 8) | (uint16_t)CL)) + +/** + * @brief Start PCA counter + * @details Set CCON register CR bit. Counter starts to count from + * the current CH/CL value. + * + * @ingroup pca + */ +#define pca_start() (CR = 1) + +/** + * @brief Stop PCA counter + * @details Clear CCON register CR bit. + * + * @ingroup pca + */ +#define pca_stop() (CR = 0) + +/** + * @brief Get PCA counter run status + * + * @return bool true if counter is running, otherwise false + * + * @ingroup pca + */ +#define is_pca_running() (CR) + +/** + * @brief Let PCA counter run in MCU idle mode + * @details Clear CMOD register CIDL bit. By default counter is gated off + * in idle mode. + * + * @ingroup pca + */ +#define pca_counter_run_in_idle() (bit_clr(CMOD, CBIT7)) + +/** + * @brief Gate off PCA counter in MCU idle mode + * @details Set CMOD register CIDL bit. + * + * @ingroup pca + */ +#define pca_counter_gate_off_in_idle() (bit_set(CMOD, SBIT7)) + +/** + * @brief Get PCA counter idle mode status + * + * @return bool true if counter is gated off in idle mode, otherwise false + * + * @ingroup pca + */ +#define is_pca_counter_gated_off_in_idle() (test_if_bit_set(CMOD, SBIT7)) + +///@} + +/** @name overflow + * PCA counter overflow flag and interrupt functions + */ +///@{ + +/** + * @brief Clear PCA counter overflow flag + * @details Clear CCON register CF bit. CF flag should be cleared by software. + * + * @ingroup pca + */ +#define pca_clear_overflow_flag() (CF = 0) + +/** + * @brief Get PCA counter overflow flag status + * + * @return bool true if counter overflow occurred, otherwise false + * + * @ingroup pca + */ +#define is_pca_overflow() (CF) + +/** + * @brief Enable PCA counter overflow interrupt + * @details Set CMOD register ECF bit. When enabled the CF flag generates + * PCA interrupt. Before call this method mcu interrupt support should be + * enabled by calling enable_mcu_interrupts() + * + * @ingroup pca + */ +#define pca_enable_overflow_interrupt() (bit_set(CMOD, SBIT0)) + +/** + * @brief Disable PCA counter overflow interrupt + * @details Clear CMOD register ECF bit. + * + * @ingroup pca + */ +#define pca_disable_overflow_interrupt() (bit_clr(CMOD, CBIT0)) + +/** + * @brief Get PCA counter overflow interrupt enable status + * + * @return bool true if overflow interrupt is enabled, otherwise false + * + * @ingroup pca + */ +#define is_pca_overflow_interrupt_enabled() (test_if_bit_set(CMOD, SBIT0)) + +///@} + +/** @name module flags + * PCA module compare/capture flag functions + */ +///@{ + +/** + * @brief Clear PCA module compare/capture flag + * @details Clear CCON register CCFn bit for given module. + * CCFn flags should be cleared by software. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_clear_module_flag(module) (CCF ## module = 0) + +/** + * @brief Get PCA module compare/capture flag status + * @details Get CCON register CCFn bit for given module. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @return bool true if match or capture occurred, otherwise false + * + * @ingroup pca + */ +#define is_pca_module_flag_set(module) (CCF ## module) + +/** + * @brief Enable PCA module compare/capture interrupt + * @details Set CCAPMn register ECCFn bit for given module. When enabled the + * CCFn flag generates PCA interrupt. Before call this method mcu interrupt + * support should be enabled by calling enable_mcu_interrupts() + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_module_enable_interrupt(module) (bit_set(CCAPM ## module, SBIT0)) + +/** + * @brief Disable PCA module compare/capture interrupt + * @details Clear CCAPMn register ECCFn bit for given module. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_module_disable_interrupt(module) (bit_clr(CCAPM ## module, CBIT0)) + +/** + * @brief Get PCA module compare/capture interrupt enable status + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @return bool true if module interrupt is enabled, otherwise false + * + * @ingroup pca + */ +#define is_pca_module_interrupt_enabled(module) (test_if_bit_set(CCAPM ## module, SBIT0)) + +///@} + +/** @name compare/capture + * PCA module compare and capture functions + */ +///@{ + +/** + * @brief Enable PCA module comparator + * @details Set CCAPMn register ECOMn bit. Comparator should be enabled for + * compare, toggle and PWM modes. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_module_enable_comparator(module) (bit_set(CCAPM ## module, SBIT6)) + +/** + * @brief Disable PCA module comparator + * @details Clear CCAPMn register ECOMn bit. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_module_disable_comparator(module) (bit_clr(CCAPM ## module, CBIT6)) + +/** + * @brief Enable capture on positive edge + * @details Set CCAPMn register CAPPn bit. Capture occurs on positive edge + * on CEXn pin. Capture and PWM modes could not be used simultaneously. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_module_enable_capture_positive(module) (bit_set(CCAPM ## module, SBIT5)) + +/** + * @brief Disable capture on positive edge + * @details Clear CCAPMn register CAPPn bit. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_module_disable_capture_positive(module) (bit_clr(CCAPM ## module, CBIT5)) + +/** + * @brief Enable capture on negative edge + * @details Set CCAPMn register CAPNn bit. Capture occurs on negative edge + * on CEXn pin. Capture and PWM modes could not be used simultaneously. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_module_enable_capture_negative(module) (bit_set(CCAPM ## module, SBIT4)) + +/** + * @brief Disable capture on negative edge + * @details Clear CCAPMn register CAPNn bit. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_module_disable_capture_negative(module) (bit_clr(CCAPM ## module, CBIT4)) + +/** + * @brief Enable PCA module match + * @details Set CCAPMn register MATn bit. When enabled a match of the PCA + * counter with module compare/capture register sets CCFn flag. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_module_enable_match(module) (bit_set(CCAPM ## module, SBIT3)) + +/** + * @brief Disable PCA module match + * @details Clear CCAPMn register MATn bit. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_module_disable_match(module) (bit_clr(CCAPM ## module, CBIT3)) + +/** + * @brief Enable PCA module toggle output + * @details Set CCAPMn register TOGn bit. When enabled a match of the PCA + * counter with module compare/capture register toggles CEXn pin. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_module_enable_toggle(module) (bit_set(CCAPM ## module, SBIT2)) + +/** + * @brief Disable PCA module toggle output + * @details Clear CCAPMn register TOGn bit. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_module_disable_toggle(module) (bit_clr(CCAPM ## module, CBIT2)) + +/** + * @brief Set PCA module compare/capture value + * @details Load 16 bit value into module CCAPnH/CCAPnL registers. + * In compare mode this value is compared with the PCA counter. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * @param value uint16_t compare value + * + * @ingroup pca + */ +#define pca_module_set_compare(module, value) \ +do { \ + CCAP ## module ## H = (uint8_t)bit_shift_right((uint16_t)(value), 8); \ + CCAP ## module ## L = (uint8_t)(value); \ +} while(0) + +/** + * @brief Get PCA module capture value + * @details Read 16 bit value from module CCAPnH/CCAPnL registers. + * In capture mode this value is the PCA counter value captured on CEXn pin + * edge. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @return uint16_t capture value + * + * @note The capture registers have no read latch, so a capture that happens + * between the two byte reads can produce a value made of one old byte and one + * new byte. Clear the CCFn flag before reading to make this window as small as + * possible. If the capture rate is high enough for this to matter, read the + * value in the PCA interrupt handler. + * + * @ingroup pca + */ +#define pca_module_get_capture(module) ((uint16_t)(bit_shift_left((uint16_t)CCAP ## module ## H, 8) | (uint16_t)CCAP ## module ## L)) + +///@} + +/** @name pwm + * PCA module pulse width modulator functions + */ +///@{ + +/** + * @brief Init PCA module in PWM mode + * @details Set PWM output resolution with module PCA_PWMn register EBSn bits + * and enable PWM output by setting CCAPMn register PWMn bit. + * + * PWM resolution could be 8, 7 or 6 bit. Note that PWMn and CAPPn/CAPNn bits + * should not be set simultaneously. + * + * Before PWM output is started the module pin should be configured as + * push-pull output and the CEXn pin function should be selected with + * pca_set_pin_group(). + * + * @param module uint8_t PCA module number 0, 1 or 2 + * @param resolution pca_pwm_resolution_t PWM resolution + * + * @ingroup pca + */ +#define pca_module_pwm_init(module, resolution) \ +do { \ + bit_clr(PCA_PWM ## module, (uint8_t)(~PCA_PWM_EBS_MSK)); \ + bit_set(PCA_PWM ## module, (uint8_t)((resolution) << PCA_PWM_EBS0_BIT)); \ + bit_set(CCAPM ## module, SBIT1); \ +} while(0) + +/** + * @brief Disable PCA module PWM output + * @details Clear CCAPMn register PWMn bit. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @ingroup pca + */ +#define pca_module_pwm_disable(module) (bit_clr(CCAPM ## module, CBIT1)) + +/** + * @brief Get PCA module PWM output enable status + * + * @param module uint8_t PCA module number 0, 1 or 2 + * + * @return bool true if PWM output is enabled, otherwise false + * + * @ingroup pca + */ +#define is_pca_module_pwm_enabled(module) (test_if_bit_set(CCAPM ## module, SBIT1)) + +/** + * @brief Set PCA module PWM duty + * @details Load duty value into module CCAPnH/CCAPnL registers. CCAPnH is + * a reload register and it is loaded into CCAPnL on PCA counter overflow, so + * new duty value is applied without glitch on PWM output. + * + * Duty value range depends on selected PWM resolution: 0..255 for 8 bit PWM, + * 0..127 for 7 bit PWM and 0..63 for 6 bit PWM. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * @param duty uint8_t PWM duty value + * + * @ingroup pca + */ +#define pca_module_pwm_set_duty(module, duty) \ +do { \ + CCAP ## module ## H = (uint8_t)(duty); \ + CCAP ## module ## L = (uint8_t)(duty); \ +} while(0) + +/** + * @brief Set PCA module PWM duty without immediate update + * @details Load duty value into module CCAPnH reload register only. New duty + * value is applied on next PCA counter overflow. Use this method to change + * duty synchronously with PWM period. + * + * @param module uint8_t PCA module number 0, 1 or 2 + * @param duty uint8_t PWM duty value + * + * @ingroup pca + */ +#define pca_module_pwm_set_duty_on_overflow(module, duty) (CCAP ## module ## H = (uint8_t)(duty)) + +///@} + +#endif diff --git a/include/spi.h b/include/spi.h new file mode 100644 index 00000000..07ec237d --- /dev/null +++ b/include/spi.h @@ -0,0 +1,732 @@ +#ifndef STC15_SPIH +#define STC15_SPIH + +/** + * @file spi.h + * + * @defgroup spi SPI + * + * @details Functions and data structures related to SPI module + * + * SPI - Serial Peripheral Interface. It is a full duplex synchronous serial + * link with a master and one or more slaves. Four wires are used: + * SS (slave select), MOSI (master output, slave input), + * MISO (master input, slave output) and SCLK (serial clock). + * + * The SPI module could work in master or slave mode. In master mode the + * clock rate is selected from the CPU clock (SYSclk) with one of the four + * available dividers. In slave mode the module is clocked by the master and + * the clock rate setting is ignored. + * + * Four clock phase and polarity combinations (modes 0..3) and either data + * order (MSB or LSB first) are supported. + * + * Transfers could be done in sync mode, which blocks until the transfer is + * finished, or in async mode. In async mode the SPI transfer complete event + * generates an interrupt. Before async transfers mcu interrupts and the SPI + * interrupt should be enabled by calling enable_spi_interrupt() from + * interrupt.h. + * + * Pin mapping is selected with spi_set_pin_group(). Three pin groups are + * available and the group selection is stored in the P_SW1 register: + * + * | Group | SS | MOSI | MISO | SCLK | + * |-------|-------|-------|-------|-------| + * | 1 | P1.2 | P1.3 | P1.4 | P1.5 | + * | 2 | P2.4 | P2.3 | P2.2 | P2.1 | + * | 3 | P5.4 | P4.0 | P4.1 | P4.3 | + * + * Note that not every pin group is available on every MCU series and package. + * For example the STC15W408AS exposes group 1 on every package, group 2 only + * on 28 pin packages and has no P4 port pins bonded out for group 3 at all. + * + * @author Michael Golovanov + */ + +#include +#include +#include +#include +#include + +/** + * @brief SPCTL register SSIG (SS ignore) bit position. + * @details If SSIG=1, MSTR decides whether the device is a master or a slave. + * If SSIG=0, the SS pin decides it. Note that if SSIG=1, CPHA must not be 0, + * otherwise the operation is undefined. + */ +#define SPI_SSIG_BIT 7 +/** @brief SPCTL register SPEN (SPI enable) bit position */ +#define SPI_SPEN_BIT 6 +/** @brief SPCTL register DORD (data order) bit position */ +#define SPI_DORD_BIT 5 +/** @brief SPCTL register MSTR (master mode select) bit position */ +#define SPI_MSTR_BIT 4 +/** @brief SPCTL register CPOL (SPI clock polarity) bit position */ +#define SPI_CPOL_BIT 3 +/** @brief SPCTL register CPHA (SPI clock phase) bit position */ +#define SPI_CPHA_BIT 2 +/** @brief SPCTL register SPR1 (clock rate select) bit position */ +#define SPI_SPR1_BIT 1 +/** @brief SPCTL register SPR0 (clock rate select) bit position */ +#define SPI_SPR0_BIT 0 + +/** @brief SPCTL register SSIG bit mask */ +#define SPI_SSIG_MSK 0x80 +/** @brief SPCTL register SPEN bit mask */ +#define SPI_SPEN_MSK 0x40 +/** @brief SPCTL register DORD bit mask */ +#define SPI_DORD_MSK 0x20 +/** @brief SPCTL register MSTR bit mask */ +#define SPI_MSTR_MSK 0x10 +/** @brief SPCTL register CPOL bit mask */ +#define SPI_CPOL_MSK 0x08 +/** @brief SPCTL register CPHA bit mask */ +#define SPI_CPHA_MSK 0x04 +/** @brief SPCTL register SPR (clock rate select) bits mask */ +#define SPI_SPR_MSK 0x03 + +/** @brief SPSTAT register SPIF (transfer complete) bit position */ +#define SPI_SPIF_BIT 7 +/** @brief SPSTAT register WCOL (write collision) bit position */ +#define SPI_WCOL_BIT 6 + +/** @brief SPSTAT register SPIF bit mask */ +#define SPI_SPIF_MSK 0x80 +/** @brief SPSTAT register WCOL bit mask */ +#define SPI_WCOL_MSK 0x40 +/** @brief SPSTAT register flags mask. Both flags are cleared by writing 1 */ +#define SPI_SPSTAT_CLEAR_MSK (SPI_SPIF_MSK | SPI_WCOL_MSK) + +/** @brief P_SW1 register SPI_S0 (SPI pin group select) bit position */ +#define SPI_S0_BIT 2 +/** @brief P_SW1 register SPI_S0 (SPI pin group select) bit mask */ +#define SPI_S0_MSK 0x04 +/** @brief P_SW1 register SPI_S1 (SPI pin group select) bit mask */ +#define SPI_S1_MSK 0x08 +/** @brief P_SW1 register SPI pin group select bits mask */ +#define SPI_SW_MSK (uint8_t)(~(SPI_S0_MSK | SPI_S1_MSK)) + +/** + * @brief SPI clock rate + * + * @details describes possible SPI clock dividers. The SPI clock is derived + * from the CPU clock (SYSclk). Used in master mode only, in slave mode the + * module is clocked by an external master. + * + * @ingroup spi + */ +typedef enum +{ + /** SYSclk / 4 */ + SPI_CLOCK_DIV_4 = 0b00000000, + /** SYSclk / 16 */ + SPI_CLOCK_DIV_16 = 0b00000001, + /** SYSclk / 64, used after MCU power on */ + SPI_CLOCK_DIV_64 = 0b00000010, + /** SYSclk / 128 */ + SPI_CLOCK_DIV_128 = 0b00000011 +} spi_clock_t; + +/** + * @brief SPI mode + * + * @details describes all four combinations of the CPOL (clock polarity) and + * CPHA (clock phase) bits. The current mode could be read back with + * spi_get_mode(). + * + * Note that in modes with CPHA=0 (SPI_MODE_0 and SPI_MODE_2) the SS pin is + * not ignored (SSIG=0), so it has to be pulled low with spi_ss_clr() before + * every byte transfer and released with spi_ss_set() after it. + * + * @ingroup spi + */ +typedef enum +{ + /** CPOL=0, CPHA=0. Clock is low when idle, data is sampled on the leading edge */ + SPI_MODE_0 = 0b00000000, + /** CPOL=0, CPHA=1. Clock is low when idle, data is changed on the leading edge */ + SPI_MODE_1 = 0b00000100, + /** CPOL=1, CPHA=0. Clock is high when idle, data is sampled on the leading edge */ + SPI_MODE_2 = 0b00001000, + /** CPOL=1, CPHA=1. Clock is high when idle, data is changed on the leading edge */ + SPI_MODE_3 = 0b00001100 +} spi_mode_t; + +/** + * @brief SPI pin group + * + * @details describes possible SS/MOSI/MISO/SCLK pin mapping variants. + * Group selection is done with spi_set_pin_group(). + * + * Note that not every pin group is available on every MCU series and package. + * For example the STC15W408AS exposes group 1 on every package, group 2 only + * on 28 pin packages and has no P4 port pins bonded out for group 3 at all. + * + * @ingroup spi + */ +typedef enum +{ + /** SS on P1.2, MOSI on P1.3, MISO on P1.4, SCLK on P1.5. Default after power on */ + SPI_PIN_GROUP_1 = 0, + /** SS_2 on P2.4, MOSI_2 on P2.3, MISO_2 on P2.2, SCLK_2 on P2.1 */ + SPI_PIN_GROUP_2 = 1, + /** SS_3 on P5.4, MOSI_3 on P4.0, MISO_3 on P4.1, SCLK_3 on P4.3 */ + SPI_PIN_GROUP_3 = 2 +} spi_pin_group_t; + +/** + * @brief Configure the SPI pins of pin group 1 for master mode + * + * @details SS on P1.2 as push-pull output and kept high, MOSI on P1.3 as + * push-pull output, MISO on P1.4 as input only, SCLK on P1.5 as push-pull + * output. + * + * Used by spi_init_master_pins(). Normally there is no need to call it + * directly. + * + * @ingroup spi + */ +#define spi_init_master_pins_SPI_PIN_GROUP_1() \ +do { \ + /* SS on P1.2 as push-pull output, kept high */ \ + pin_push_pull_init(P1, 2); \ + bit_set(P1, SBIT2); \ + /* MOSI on P1.3 as push-pull output */ \ + pin_push_pull_init(P1, 3); \ + /* MISO on P1.4 as input only */ \ + pin_input_only_init(P1, 4); \ + /* SCLK on P1.5 as push-pull output */ \ + pin_push_pull_init(P1, 5); \ +} while(0) + +/** + * @brief Configure the SPI pins of pin group 2 for master mode + * + * @details SS_2 on P2.4 as push-pull output and kept high, MOSI_2 on P2.3 as + * push-pull output, MISO_2 on P2.2 as input only, SCLK_2 on P2.1 as push-pull + * output. + * + * Used by spi_init_master_pins(). Normally there is no need to call it + * directly. + * + * @ingroup spi + */ +#define spi_init_master_pins_SPI_PIN_GROUP_2() \ +do { \ + /* SS_2 on P2.4 as push-pull output, kept high */ \ + pin_push_pull_init(P2, 4); \ + bit_set(P2, SBIT4); \ + /* MOSI_2 on P2.3 as push-pull output */ \ + pin_push_pull_init(P2, 3); \ + /* MISO_2 on P2.2 as input only */ \ + pin_input_only_init(P2, 2); \ + /* SCLK_2 on P2.1 as push-pull output */ \ + pin_push_pull_init(P2, 1); \ +} while(0) + +/** + * @brief Configure the SPI pins of pin group 3 for master mode + * + * @details SS_3 on P5.4 as push-pull output and kept high, MOSI_3 on P4.0 as + * push-pull output, MISO_3 on P4.1 as input only, SCLK_3 on P4.3 as push-pull + * output. + * + * Used by spi_init_master_pins(). Normally there is no need to call it + * directly. + * + * @note Pin group 3 is not available on the STC15W401AS series, which has no + * P4 port pins bonded out. + * + * @ingroup spi + */ +#define spi_init_master_pins_SPI_PIN_GROUP_3() \ +do { \ + /* SS_3 on P5.4 as push-pull output, kept high */ \ + pin_push_pull_init(P5, 4); \ + bit_set(P5, SBIT4); \ + /* MOSI_3 on P4.0 as push-pull output */ \ + pin_push_pull_init(P4, 0); \ + /* MISO_3 on P4.1 as input only */ \ + pin_input_only_init(P4, 1); \ + /* SCLK_3 on P4.3 as push-pull output */ \ + pin_push_pull_init(P4, 3); \ +} while(0) + +/** + * @brief Configure the SPI pins of pin group 1 for slave mode + * + * @details SS on P1.2 as input only, MOSI on P1.3 as input only, MISO on + * P1.4 as push-pull output, SCLK on P1.5 as input only. + * + * Used by spi_init_slave_pins(). Normally there is no need to call it + * directly. + * + * @ingroup spi + */ +#define spi_init_slave_pins_SPI_PIN_GROUP_1() \ +do { \ + /* SS on P1.2 as input only */ \ + pin_input_only_init(P1, 2); \ + /* MOSI on P1.3 as input only */ \ + pin_input_only_init(P1, 3); \ + /* MISO on P1.4 as push-pull output */ \ + pin_push_pull_init(P1, 4); \ + /* SCLK on P1.5 as input only */ \ + pin_input_only_init(P1, 5); \ +} while(0) + +/** + * @brief Configure the SPI pins of pin group 2 for slave mode + * + * @details SS_2 on P2.4 as input only, MOSI_2 on P2.3 as input only, MISO_2 + * on P2.2 as push-pull output, SCLK_2 on P2.1 as input only. + * + * Used by spi_init_slave_pins(). Normally there is no need to call it + * directly. + * + * @ingroup spi + */ +#define spi_init_slave_pins_SPI_PIN_GROUP_2() \ +do { \ + /* SS_2 on P2.4 as input only */ \ + pin_input_only_init(P2, 4); \ + /* MOSI_2 on P2.3 as input only */ \ + pin_input_only_init(P2, 3); \ + /* MISO_2 on P2.2 as push-pull output */ \ + pin_push_pull_init(P2, 2); \ + /* SCLK_2 on P2.1 as input only */ \ + pin_input_only_init(P2, 1); \ +} while(0) + +/** + * @brief Configure the SPI pins of pin group 3 for slave mode + * + * @details SS_3 on P5.4 as input only, MOSI_3 on P4.0 as input only, MISO_3 + * on P4.1 as push-pull output, SCLK_3 on P4.3 as input only. + * + * Used by spi_init_slave_pins(). Normally there is no need to call it + * directly. + * + * @note Pin group 3 is not available on the STC15W401AS series, which has no + * P4 port pins bonded out. + * + * @ingroup spi + */ +#define spi_init_slave_pins_SPI_PIN_GROUP_3() \ +do { \ + /* SS_3 on P5.4 as input only */ \ + pin_input_only_init(P5, 4); \ + /* MOSI_3 on P4.0 as input only */ \ + pin_input_only_init(P4, 0); \ + /* MISO_3 on P4.1 as push-pull output */ \ + pin_push_pull_init(P4, 1); \ + /* SCLK_3 on P4.3 as input only */ \ + pin_input_only_init(P4, 3); \ +} while(0) + +/** + * @brief Pull the SS pin of pin group 1 low + * + * @details SS on P1.2 is configured as a push-pull output by + * spi_init_master_pins_SPI_PIN_GROUP_1() + * + * @ingroup spi + */ +#define spi_ss_clr_SPI_PIN_GROUP_1() (bit_clr(P1, CBIT2)) + +/** + * @brief Pull the SS pin of pin group 1 high + * + * @ingroup spi + */ +#define spi_ss_set_SPI_PIN_GROUP_1() (bit_set(P1, SBIT2)) + +/** + * @brief Pull the SS pin of pin group 2 low + * + * @details SS_2 on P2.4 is configured as a push-pull output by + * spi_init_master_pins_SPI_PIN_GROUP_2() + * + * @ingroup spi + */ +#define spi_ss_clr_SPI_PIN_GROUP_2() (bit_clr(P2, CBIT4)) + +/** + * @brief Pull the SS pin of pin group 2 high + * + * @ingroup spi + */ +#define spi_ss_set_SPI_PIN_GROUP_2() (bit_set(P2, SBIT4)) + +/** + * @brief Pull the SS pin of pin group 3 low + * + * @details SS_3 on P5.4 is configured as a push-pull output by + * spi_init_master_pins_SPI_PIN_GROUP_3() + * + * @note Pin group 3 is not available on the STC15W401AS series. + * + * @ingroup spi + */ +#define spi_ss_clr_SPI_PIN_GROUP_3() (bit_clr(P5, CBIT4)) + +/** + * @brief Pull the SS pin of pin group 3 high + * + * @ingroup spi + */ +#define spi_ss_set_SPI_PIN_GROUP_3() (bit_set(P5, SBIT4)) + +/** @name init + * SPI initialization routines + */ +///@{ + +/** + * @brief Configure the SPI pins for master mode + * + * @details Configures the SS, MOSI and SCLK pins as push-pull outputs and + * the MISO pin as input only for the given pin group. The SS pin is kept + * high, so an external slave stays deselected and the SPI is not switched + * to slave mode by the SS pin. + * + * @param group spi_pin_group_t pin group to use. Must be a compile time + * constant, for example SPI_PIN_GROUP_1 + * + * @ingroup spi + */ +#define spi_init_master_pins(group) spi_init_master_pins_##group() + +/** + * @brief Configure the SPI pins for slave mode + * + * @details Configures the SS, MOSI and SCLK pins as input only and the MISO + * pin as push-pull output for the given pin group. + * + * @param group spi_pin_group_t pin group to use. Must be a compile time + * constant, for example SPI_PIN_GROUP_1 + * + * @ingroup spi + */ +#define spi_init_slave_pins(group) spi_init_slave_pins_##group() + +/** + * @brief SPI initialization in master mode + * + * @details Selects the pin group with spi_set_pin_group(), configures the + * SS, MOSI, MISO and SCLK pins and writes the SPCTL register with the SPI + * enable, master mode, clock divider, mode and data order bits. + * + * The SS pin is configured as an output and kept high. In modes with CPHA=0 + * (SPI_MODE_0 and SPI_MODE_2) the SS pin is not ignored (SSIG=0), so it has + * to be pulled low with spi_ss_clr() before every byte transfer and released + * with spi_ss_set() after it. In modes with CPHA=1 the SS pin is ignored + * (SSIG=1) and could be left high all the time. + * + * @param group spi_pin_group_t pin group to use. Must be a compile time + * constant, for example SPI_PIN_GROUP_1 + * @param mode spi_mode_t SPI mode (CPOL and CPHA combination) + * @param clock spi_clock_t SPI clock divider, SPI clock is derived from SYSclk + * @param lsb_first bool data order, true transmits LSB first, false MSB first + * + * @ingroup spi + */ +#define spi_init_master(group, mode, clock, lsb_first) \ +do { \ + /* Select the SS/MOSI/MISO/SCLK pin group */ \ + spi_set_pin_group(group); \ + /* Configure the SS/MOSI/MISO/SCLK pins */ \ + spi_init_master_pins(group); \ + /* Enable SPI as a master with the given mode, clock and data order */ \ + SPCTL = SPI_SPEN_MSK | SPI_MSTR_MSK | (mode) | (clock) | \ + ((lsb_first) ? SPI_DORD_MSK : 0) | \ + (((mode) & SPI_CPHA_MSK) ? SPI_SSIG_MSK : 0); \ +} while(0) + +/** + * @brief SPI initialization in slave mode + * + * @details Selects the pin group with spi_set_pin_group(), configures the + * SS, MOSI, MISO and SCLK pins and writes the SPCTL register with the SPI + * enable, mode and data order bits. The SS pin always selects the device + * (SSIG=0) and the SPI is clocked by an external master, so the clock rate + * setting is not used. + * + * @param group spi_pin_group_t pin group to use. Must be a compile time + * constant, for example SPI_PIN_GROUP_1 + * @param mode spi_mode_t SPI mode (CPOL and CPHA combination) + * @param lsb_first bool data order, true transmits LSB first, false MSB first + * + * @ingroup spi + */ +#define spi_init_slave(group, mode, lsb_first) \ +do { \ + /* Select the SS/MOSI/MISO/SCLK pin group */ \ + spi_set_pin_group(group); \ + /* Configure the SS/MOSI/MISO/SCLK pins */ \ + spi_init_slave_pins(group); \ + /* Enable SPI as a slave with the given mode and data order */ \ + SPCTL = SPI_SPEN_MSK | (mode) | ((lsb_first) ? SPI_DORD_MSK : 0); \ +} while(0) + +/** + * @brief Deinitialize the SPI module + * + * @details Disables the SPI interface by clearing the SPEN bit and clears + * the transfer complete and write collision flags. After that the SPI pins + * function as normal I/O port pins, but their configured modes (push-pull + * output, input only) are kept. + * + * @ingroup spi + */ +#define spi_destroy(void) \ +do { \ + /* Disable the SPI interface */ \ + SPCTL = 0; \ + /* Clear the SPIF and WCOL flags */ \ + bit_set(SPSTAT, SPI_SPSTAT_CLEAR_MSK); \ +} while(0) + +///@} + +/** @name pin group + * SPI pin mapping functions + */ +///@{ + +/** + * @brief Set the SS/MOSI/MISO/SCLK pin group + * @details Select which pins are used as SS, MOSI, MISO and SCLK. + * Pins are selected with the P_SW1 register SPI_S1 and SPI_S0 bits. + * + * By default (after MCU power on) pin group 1 is used. + * + * @param group spi_pin_group_t pin group to use + * + * @ingroup spi + */ +#define spi_set_pin_group(group) \ +do { \ + bit_clr(P_SW1, SPI_SW_MSK); \ + bit_set(P_SW1, (uint8_t)((group) << SPI_S0_BIT)); \ +} while(0) + +/** + * @brief Get the SS/MOSI/MISO/SCLK pin group + * + * @return spi_pin_group_t current pin group + * + * @ingroup spi + */ +#define spi_get_pin_group() ((spi_pin_group_t)(bit_shift_right(get_reg(P_SW1, (SPI_S0_MSK | SPI_S1_MSK)), SPI_S0_BIT))) + +///@} + +/** @name slave select + * SPI slave select (SS) pin handling in master mode + */ +///@{ + +/** + * @brief Select an external slave + * + * @details Pulls the SS pin low. The SS pin is configured as a push-pull + * output by spi_init_master(). + * + * @param group spi_pin_group_t pin group in use. Must be the literal enum + * constant, for example SPI_PIN_GROUP_1, not a variable: the pin group is + * selected at compile time by token pasting, so unlike + * spi_set_pin_group() this routine cannot take a runtime value + * + * @ingroup spi + */ +#define spi_ss_clr(group) spi_ss_clr_##group() + +/** + * @brief Deselect an external slave + * + * @details Pulls the SS pin high. The SS pin is configured as a push-pull + * output by spi_init_master(). + * + * @param group spi_pin_group_t pin group in use. Must be the literal enum + * constant, for example SPI_PIN_GROUP_1, not a variable: the pin group is + * selected at compile time by token pasting, so unlike + * spi_set_pin_group() this routine cannot take a runtime value + * + * @ingroup spi + */ +#define spi_ss_set(group) spi_ss_set_##group() + +///@} + +/** @name sync + * SPI synchronous transfer functions + */ +///@{ + +/** + * @brief Write a byte over SPI + * + * @details Clears the status flags, writes a byte into the SPI data register, + * which starts the transfer, and blocks until the transfer is finished. + * The received byte is discarded. + * + * @param data uint8_t byte to transmit + * + * @ingroup spi + */ +#define spi_write_sync(data) \ +do { \ + /* Clear the SPIF and WCOL flags */ \ + bit_set(SPSTAT, SPI_SPSTAT_CLEAR_MSK); \ + /* Writing the data register starts the transfer */ \ + SPDAT = (data); \ + /* Wait until the transfer is finished */ \ + while (test_if_bit_cleared(SPSTAT, SPI_SPIF_MSK)); \ + /* Clear the SPIF and WCOL flags */ \ + bit_set(SPSTAT, SPI_SPSTAT_CLEAR_MSK); \ +} while(0) + +/** + * @brief Transfer a byte over SPI + * + * @details Writes a byte into the SPI data register, which starts the + * transfer, blocks until the transfer is finished and stores the received + * byte. + * + * @param value uint8_t* received byte + * @param data uint8_t byte to transmit + * + * @ingroup spi + */ +#define spi_transfer_sync(value, data) \ +do { \ + spi_write_sync(data); \ + /* Store the received byte */ \ + *(value) = SPDAT; \ +} while(0) + +///@} + +/** @name async + * SPI asynchronous transfer functions + */ +///@{ + +/** + * @brief Start an asynchronous SPI transfer + * + * @details Clears the status flags and writes a byte into the SPI data + * register, which starts the transfer. The program is not blocked after + * that. When the transfer is finished the MCU generates an SPI interrupt. + * + * @note MCU interrupts and the SPI interrupt should be enabled before async + * transfers by calling enable_spi_interrupt() from interrupt.h + * + * @param data uint8_t byte to transmit + * + * @ingroup spi + */ +#define spi_async_transfer_start(data) \ +do { \ + /* Clear the SPIF and WCOL flags */ \ + bit_set(SPSTAT, SPI_SPSTAT_CLEAR_MSK); \ + /* Writing the data register starts the transfer */ \ + SPDAT = (data); \ +} while(0) + +/** + * @brief Get the SPI asynchronous transfer status + * + * @return bool true if the transfer is finished, otherwise false + * + * @ingroup spi + */ +#define is_spi_async_transfer_complete() (test_if_bit_set(SPSTAT, SPI_SPIF_MSK)) + +/** + * @brief Clear the SPI transfer complete and write collision flags + * + * @note This routine is supposed to be called inside the SPI interrupt + * handler + * + * @ingroup spi + */ +#define spi_async_transfer_finish() (bit_set(SPSTAT, SPI_SPSTAT_CLEAR_MSK)) + +/** + * @brief Get the byte received in the asynchronous transfer + * + * @details The SPI data register is double buffered, so it holds the byte + * received during the last completed transfer. + * + * This routine is supposed to be used inside the SPI interrupt handler. + * Typically spi_async_transfer_finish() is called after it to clear the + * transfer complete flag. + * + * @return uint8_t received byte + * + * @ingroup spi + */ +#define spi_async_get_result() (SPDAT) + +///@} + +/** @name status + * SPI state functions + */ +///@{ + +/** + * @brief Get the SPI enable status + * + * @return bool true if the SPI interface is enabled, otherwise false + * + * @ingroup spi + */ +#define is_spi_enabled() (test_if_bit_set(SPCTL, SPI_SPEN_MSK)) + +/** + * @brief Get the SPI master/slave mode status + * + * @return bool true if the SPI works in master mode, otherwise false + * + * @ingroup spi + */ +#define is_spi_master() (test_if_bit_set(SPCTL, SPI_MSTR_MSK)) + +/** + * @brief Get the SPI write collision status + * + * @details The WCOL flag is set if the SPI data register is written while a + * transfer is still in progress. The flag is cleared by + * spi_async_transfer_finish() or by spi_write_sync(). + * + * @return bool true if a write collision happened, otherwise false + * + * @ingroup spi + */ +#define is_spi_write_collision() (test_if_bit_set(SPSTAT, SPI_WCOL_MSK)) + +/** + * @brief Get the SPI mode + * + * @return spi_mode_t current CPOL and CPHA combination + * + * @ingroup spi + */ +#define spi_get_mode() ((spi_mode_t)get_reg(SPCTL, (SPI_CPOL_MSK | SPI_CPHA_MSK))) + +/** + * @brief Get the SPI clock divider + * + * @return spi_clock_t current SPI clock divider + * + * @ingroup spi + */ +#define spi_get_clock() ((spi_clock_t)get_reg(SPCTL, SPI_SPR_MSK)) + +///@} + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 00000000..6b968883 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,64 @@ +# Host tests for the library headers. +# +# The rest of this project is cross compiled for the 8051 with SDCC, but the +# tests in this directory run on the build host. They verify the pure logic of +# the HAL headers (bit masks, shifts, token pasting, enum encodings) by +# compiling the real headers against a host stub for the SDCC , +# then asserting on the register values the macros produce. +# +# A custom command is used instead of add_executable on purpose: the parent +# project sets CMAKE_C_COMPILER to SDCC, so an ordinary target here would be +# cross compiled into an mcs51 binary that cannot run on the build host. The +# custom command invokes the host compiler directly and leaves the SDCC +# toolchain untouched. +# +# Add a new test by appending its name to HOST_TESTS; the source is expected at +# .c in this directory. + +set(HOST_TESTS + test_pca + test_spi +) + +find_program(HOST_CC NAMES cc gcc clang) + +if(NOT HOST_CC) + message(WARNING "No host C compiler found, host tests will not be built") + return() +endif() + +set(HOST_TEST_BIN_DIR ${CMAKE_BINARY_DIR}/test) + +file(MAKE_DIRECTORY ${HOST_TEST_BIN_DIR}) + +add_custom_target(host_tests ALL + COMMENT "Building host tests" +) + +foreach(test IN LISTS HOST_TESTS) + set(test_source ${CMAKE_CURRENT_SOURCE_DIR}/${test}.c) + set(test_binary ${HOST_TEST_BIN_DIR}/${test}) + + add_custom_command( + OUTPUT ${test_binary} + COMMAND ${HOST_CC} + -std=c23 -Wall -Wextra + -I ${CMAKE_CURRENT_SOURCE_DIR}/stubs + -I ${CMAKE_SOURCE_DIR}/include + -o ${test_binary} + ${test_source} + DEPENDS ${test_source} + ${CMAKE_CURRENT_SOURCE_DIR}/stubs/compiler.h + COMMENT "Building host test ${test}" + VERBATIM + ) + + add_custom_target(${test}_build ALL DEPENDS ${test_binary}) + + add_dependencies(host_tests ${test}_build) + + add_test( + NAME ${test} + COMMAND ${test_binary} + ) +endforeach() diff --git a/test/README b/test/README index 5000a7dc..402c4fea 100644 --- a/test/README +++ b/test/README @@ -1,4 +1,30 @@ -This directory is intended for PlatformIO Test Runner and project tests. +This directory contains host tests for the library headers, and is also the +location PlatformIO Test Runner expects project tests in. -STC15 library does not contains tests. \ No newline at end of file +The tests compile the real headers with a host C compiler and assert the +register values the HAL macros produce. They cover the parts of a module that +are pure logic: bit masks, shifts, token pasting and enum encodings. They do +not cover 8051 hardware behaviour, which cannot be observed on the host. + +Files: + + test_pca.c tests for include/pca.h + test_spi.c tests for include/spi.h + stubs/compiler.h host stub for the SDCC , see the notes in it + CMakeLists.txt registers the tests with ctest + +Run the tests through the CMake build: + + cmake -B build + cmake --build build --target host_tests + ctest --test-dir build -V + +Adding a test: create .c in this directory and append to +HOST_TESTS in CMakeLists.txt. + +Note that the tests deliberately do not go through the SDCC toolchain used by +the rest of the project. The parent CMakeLists sets CMAKE_C_COMPILER to SDCC, +so a test built as a normal CMake target would be cross compiled into an mcs51 +binary that cannot run on the build host. test/CMakeLists.txt invokes the host +compiler through a custom command instead. diff --git a/test/test_pca.c b/test/test_pca.c new file mode 100644 index 00000000..32ab17cd --- /dev/null +++ b/test/test_pca.c @@ -0,0 +1,329 @@ +/* + * Host tests for the PCA/CCP HAL (include/pca.h). + * + * These tests compile the real header against test/stubs/compiler.h and assert + * the exact register values the macros produce. They cover the parts of the + * module that are pure logic: bit masks, shifts, enum encodings and token + * pasting. They do not and cannot verify 8051 hardware behaviour (see the + * limitations noted in test/stubs/compiler.h). + * + * Expected values are derived from the STC15 series datasheet register tables: + * + * CMOD D9H CIDL - - - CPS2 CPS1 CPS0 ECF + * CCON D8H CF CR - - - CCF2 CCF1 CCF0 + * CCAPMn DAH - ECOMn CAPPn CAPNn MATn TOGn PWMn ECCFn + * PCA_PWMn F2H EBSn_1 EBSn_0 - - - - EPCnH EPCnL + * P_SW1 A2H S1_S1 S1_S0 CCP_S1 CCP_S0 SPI_S1 SPI_S0 0 DPS + * + * Run: ctest --test-dir -V (see test/CMakeLists.txt) + */ + +#include +#include +#include + +#include + +static int failures = 0; +static int checks = 0; + +#define CHECK_EQ(actual, expected, what) \ +do { \ + checks++; \ + unsigned long a_ = (unsigned long)(actual); \ + unsigned long e_ = (unsigned long)(expected); \ + if (a_ != e_) { \ + failures++; \ + printf("FAIL %s:%d %s: got 0x%02lX, expected 0x%02lX\n", \ + __FILE__, __LINE__, (what), a_, e_); \ + } \ +} while(0) + +/* ---------------------------------------------------------------- pin group */ + +static void test_pin_group(void) +{ + /* CCP_S1:CCP_S0 are P_SW1 bits 5:4. Selecting a group must clear both + bits first, then set the requested value, leaving other bits alone. */ + P_SW1 = 0xFF; + pca_set_pin_group(PCA_PIN_GROUP_1); /* 00 */ + CHECK_EQ(P_SW1, 0xCF, "P_SW1 group 1 clears CCP bits, keeps others"); + + P_SW1 = 0x00; + pca_set_pin_group(PCA_PIN_GROUP_2); /* 01 -> bit4 */ + CHECK_EQ(P_SW1, 0x10, "P_SW1 group 2 sets bit 4"); + + P_SW1 = 0x00; + pca_set_pin_group(PCA_PIN_GROUP_3); /* 10 -> bit5 */ + CHECK_EQ(P_SW1, 0x20, "P_SW1 group 3 sets bit 5"); + + /* Enum encodings match the datasheet bit patterns. */ + CHECK_EQ(PCA_PIN_GROUP_1, 0, "PCA_PIN_GROUP_1 encoding"); + CHECK_EQ(PCA_PIN_GROUP_2, 1, "PCA_PIN_GROUP_2 encoding"); + CHECK_EQ(PCA_PIN_GROUP_3, 2, "PCA_PIN_GROUP_3 encoding"); + + /* Read back through the getter. */ + P_SW1 = 0x00; + pca_set_pin_group(PCA_PIN_GROUP_3); + CHECK_EQ(pca_get_pin_group(), PCA_PIN_GROUP_3, "pca_get_pin_group reads 3"); + pca_set_pin_group(PCA_PIN_GROUP_2); + CHECK_EQ(pca_get_pin_group(), PCA_PIN_GROUP_2, "pca_get_pin_group reads 2"); + pca_set_pin_group(PCA_PIN_GROUP_1); + CHECK_EQ(pca_get_pin_group(), PCA_PIN_GROUP_1, "pca_get_pin_group reads 1"); +} + +/* ------------------------------------------------------------ clock source */ + +static void test_clock_source(void) +{ + /* CPS2:CPS1:CPS0 are CMOD bits 3:1. */ + CMOD = 0x00; + pca_set_clock_source(PCA_CLOCK_SYS); /* 4 -> 4<<1 = 0x08 */ + CHECK_EQ(CMOD, 0x08, "CMOD SYSclk"); + + CMOD = 0x00; + pca_set_clock_source(PCA_CLOCK_SYS_12); /* 0 */ + CHECK_EQ(CMOD, 0x00, "CMOD SYSclk/12"); + + CMOD = 0x00; + pca_set_clock_source(PCA_CLOCK_SYS_8); /* 7 -> 0x0E */ + CHECK_EQ(CMOD, 0x0E, "CMOD SYSclk/8"); + + /* ECF (bit 0) and CIDL (bit 7) must survive a clock source change. */ + CMOD = 0x81; /* CIDL=1, ECF=1 */ + pca_set_clock_source(PCA_CLOCK_TIMER0); /* 2 -> 0x04 */ + CHECK_EQ(CMOD, 0x85, "CMOD keeps CIDL and ECF bits"); + + /* Enum encodings match the datasheet CPS values. */ + CHECK_EQ(PCA_CLOCK_SYS_12, 0, "PCA_CLOCK_SYS_12 encoding"); + CHECK_EQ(PCA_CLOCK_SYS_2, 1, "PCA_CLOCK_SYS_2 encoding"); + CHECK_EQ(PCA_CLOCK_TIMER0, 2, "PCA_CLOCK_TIMER0 encoding"); + CHECK_EQ(PCA_CLOCK_ECI, 3, "PCA_CLOCK_ECI encoding"); + CHECK_EQ(PCA_CLOCK_SYS, 4, "PCA_CLOCK_SYS encoding"); + CHECK_EQ(PCA_CLOCK_SYS_4, 5, "PCA_CLOCK_SYS_4 encoding"); + CHECK_EQ(PCA_CLOCK_SYS_6, 6, "PCA_CLOCK_SYS_6 encoding"); + CHECK_EQ(PCA_CLOCK_SYS_8, 7, "PCA_CLOCK_SYS_8 encoding"); + + /* Round trip through the getter for every source. */ + for (int src = 0; src <= 7; src++) { + CMOD = 0x00; + pca_set_clock_source((pca_clock_source_t)src); + CHECK_EQ(pca_get_clock_source(), (unsigned)src, + "pca_get_clock_source round trip"); + } +} + +/* ------------------------------------------------------------ counter value */ + +static void test_counter(void) +{ + /* CH is the high byte, CL the low byte. */ + pca_set_counter(0x1234); + CHECK_EQ(CH, 0x12, "pca_set_counter high byte"); + CHECK_EQ(CL, 0x34, "pca_set_counter low byte"); + + CHECK_EQ(pca_get_counter(), 0x1234, "pca_get_counter reads back"); + + pca_set_counter(0x00FF); + CHECK_EQ(CH, 0x00, "pca_set_counter 0x00FF high byte"); + CHECK_EQ(CL, 0xFF, "pca_set_counter 0x00FF low byte"); + CHECK_EQ(pca_get_counter(), 0x00FF, "pca_get_counter 0x00FF"); + + pca_set_counter(0xFFFF); + CHECK_EQ(pca_get_counter(), 0xFFFF, "pca_get_counter 0xFFFF"); + + pca_set_counter(0x0000); + CHECK_EQ(pca_get_counter(), 0x0000, "pca_get_counter 0x0000"); +} + +/* ------------------------------------------------------- run / idle control */ + +static void test_run_and_idle(void) +{ + /* CR is CCON bit 6, exposed as the named bit CR in STC15Fxx.h. */ + pca_stop(); + CHECK_EQ(is_pca_running(), 0, "is_pca_running false after stop"); + pca_start(); + CHECK_EQ(is_pca_running(), 1, "is_pca_running true after start"); + + /* CIDL is CMOD bit 7. */ + pca_counter_run_in_idle(); + CHECK_EQ(CMOD & 0x80, 0x00, "CIDL cleared by run_in_idle"); + CHECK_EQ(is_pca_counter_gated_off_in_idle(), 0, "gated_off false after run_in_idle"); + + pca_counter_gate_off_in_idle(); + CHECK_EQ(CMOD & 0x80, 0x80, "CIDL set by gate_off_in_idle"); + CHECK_EQ(is_pca_counter_gated_off_in_idle(), 1, "gated_off true after gate_off_in_idle"); + + /* ECF is CMOD bit 0. */ + pca_disable_overflow_interrupt(); + CHECK_EQ(CMOD & 0x01, 0x00, "ECF cleared"); + CHECK_EQ(is_pca_overflow_interrupt_enabled(), 0, "overflow int disabled"); + pca_enable_overflow_interrupt(); + CHECK_EQ(CMOD & 0x01, 0x01, "ECF set"); + CHECK_EQ(is_pca_overflow_interrupt_enabled(), 1, "overflow int enabled"); + + /* CF is CCON bit 7, cleared through the named bit. */ + CF = 1; + CHECK_EQ(is_pca_overflow(), 1, "overflow flag set"); + pca_clear_overflow_flag(); + CHECK_EQ(is_pca_overflow(), 0, "overflow flag cleared"); +} + +/* ------------------------------------------------------- module bit control */ + +static void test_module_bits(void) +{ + /* Each module has its own CCAPMn register with identical bit layout. */ + CCAPM0 = 0x00; + pca_module_enable_comparator(0); /* ECOMn = bit 6 */ + CHECK_EQ(CCAPM0, 0x40, "module 0 ECOM bit 6"); + pca_module_disable_comparator(0); + CHECK_EQ(CCAPM0, 0x00, "module 0 ECOM cleared"); + + CCAPM1 = 0x00; + pca_module_enable_capture_positive(1); /* CAPPn = bit 5 */ + CHECK_EQ(CCAPM1, 0x20, "module 1 CAPP bit 5"); + pca_module_disable_capture_positive(1); + CHECK_EQ(CCAPM1, 0x00, "module 1 CAPP cleared"); + + CCAPM2 = 0x00; + pca_module_enable_capture_negative(2); /* CAPNn = bit 4 */ + CHECK_EQ(CCAPM2, 0x10, "module 2 CAPN bit 4"); + pca_module_disable_capture_negative(2); + CHECK_EQ(CCAPM2, 0x00, "module 2 CAPN cleared"); + + CCAPM0 = 0x00; + pca_module_enable_match(0); /* MATn = bit 3 */ + CHECK_EQ(CCAPM0, 0x08, "module 0 MAT bit 3"); + pca_module_disable_match(0); + CHECK_EQ(CCAPM0, 0x00, "module 0 MAT cleared"); + + CCAPM1 = 0x00; + pca_module_enable_toggle(1); /* TOGn = bit 2 */ + CHECK_EQ(CCAPM1, 0x04, "module 1 TOG bit 2"); + pca_module_disable_toggle(1); + CHECK_EQ(CCAPM1, 0x00, "module 1 TOG cleared"); + + /* Interrupt enable is ECCFn = bit 0. */ + CCAPM2 = 0x00; + pca_module_enable_interrupt(2); + CHECK_EQ(CCAPM2, 0x01, "module 2 ECCF bit 0"); + CHECK_EQ(is_pca_module_interrupt_enabled(2), 1, "module 2 int enabled"); + pca_module_disable_interrupt(2); + CHECK_EQ(CCAPM2, 0x00, "module 2 ECCF cleared"); + CHECK_EQ(is_pca_module_interrupt_enabled(2), 0, "module 2 int disabled"); + + /* The token pasting must reach the right register per module. */ + CCAPM0 = 0x00; CCAPM1 = 0x00; CCAPM2 = 0x00; + pca_module_enable_comparator(1); + CHECK_EQ(CCAPM0, 0x00, "module 1 ECOM does not touch CCAPM0"); + CHECK_EQ(CCAPM1, 0x40, "module 1 ECOM hits CCAPM1"); + CHECK_EQ(CCAPM2, 0x00, "module 1 ECOM does not touch CCAPM2"); + + /* CCON module flags CCF0..CCF2 are bits 0..2. */ + CCF0 = 1; CCF1 = 0; CCF2 = 0; + CHECK_EQ(is_pca_module_flag_set(0), 1, "CCF0 set"); + CHECK_EQ(is_pca_module_flag_set(1), 0, "CCF1 clear"); + pca_clear_module_flag(0); + CHECK_EQ(is_pca_module_flag_set(0), 0, "CCF0 cleared"); +} + +/* ---------------------------------------------------------- compare/capture */ + +static void test_compare_capture(void) +{ + /* CCAPnH is the high byte, CCAPnL the low byte. */ + pca_module_set_compare(0, 0xABCD); + CHECK_EQ(CCAP0H, 0xAB, "module 0 compare high byte"); + CHECK_EQ(CCAP0L, 0xCD, "module 0 compare low byte"); + + pca_module_set_compare(1, 0x1234); + CHECK_EQ(CCAP1H, 0x12, "module 1 compare high byte"); + CHECK_EQ(CCAP1L, 0x34, "module 1 compare low byte"); + + pca_module_set_compare(2, 0x00FF); + CHECK_EQ(CCAP2H, 0x00, "module 2 compare high byte"); + CHECK_EQ(CCAP2L, 0xFF, "module 2 compare low byte"); + + CHECK_EQ(pca_module_get_capture(0), 0xABCD, "module 0 capture read back"); + CHECK_EQ(pca_module_get_capture(1), 0x1234, "module 1 capture read back"); + CHECK_EQ(pca_module_get_capture(2), 0x00FF, "module 2 capture read back"); + + /* Token pasting must not cross registers. */ + pca_module_set_compare(2, 0xFFFF); + CHECK_EQ(CCAP0H, 0xAB, "module 2 write leaves module 0 high untouched"); + CHECK_EQ(CCAP1H, 0x12, "module 2 write leaves module 1 high untouched"); +} + +/* -------------------------------------------------------------------- pwm */ + +static void test_pwm(void) +{ + /* EBSn_1:EBSn_0 are PCA_PWMn bits 7:6; PWMn is CCAPMn bit 1. + 00 = 8 bit, 01 = 7 bit, 10 = 6 bit. */ + PCA_PWM0 = 0xFF; + CCAPM0 = 0x00; + pca_module_pwm_init(0, PCA_PWM_8BIT); + CHECK_EQ(PCA_PWM0, 0x3F, "8 bit PWM clears EBS bits"); + CHECK_EQ(CCAPM0, 0x02, "8 bit PWM sets PWMn bit 1"); + + PCA_PWM1 = 0xFF; + CCAPM1 = 0x00; + pca_module_pwm_init(1, PCA_PWM_7BIT); + CHECK_EQ(PCA_PWM1, 0x7F, "7 bit PWM sets EBS0 only"); + CHECK_EQ(CCAPM1, 0x02, "7 bit PWM sets PWMn bit 1"); + + PCA_PWM2 = 0xFF; + CCAPM2 = 0x00; + pca_module_pwm_init(2, PCA_PWM_6BIT); + CHECK_EQ(PCA_PWM2, 0xBF, "6 bit PWM sets EBS1 only"); + CHECK_EQ(CCAPM2, 0x02, "6 bit PWM sets PWMn bit 1"); + + /* Enum encodings match the EBS bit patterns. */ + CHECK_EQ(PCA_PWM_8BIT, 0, "PCA_PWM_8BIT encoding"); + CHECK_EQ(PCA_PWM_7BIT, 1, "PCA_PWM_7BIT encoding"); + CHECK_EQ(PCA_PWM_6BIT, 2, "PCA_PWM_6BIT encoding"); + + /* PWM init must not disturb unrelated CCAPMn bits (e.g. ECOM). */ + CCAPM0 = 0x40; + pca_module_pwm_init(0, PCA_PWM_8BIT); + CHECK_EQ(CCAPM0 & 0x40, 0x40, "PWM init preserves ECOM"); + + /* Duty is written to both CCAPnH and CCAPnL. */ + pca_module_pwm_set_duty(0, 0x80); + CHECK_EQ(CCAP0H, 0x80, "duty high byte"); + CHECK_EQ(CCAP0L, 0x80, "duty low byte"); + + pca_module_pwm_set_duty(1, 0x40); + CHECK_EQ(CCAP1H, 0x40, "module 1 duty high byte"); + CHECK_EQ(CCAP1L, 0x40, "module 1 duty low byte"); + + /* Overflow-only update touches the reload register alone. */ + CCAP2H = 0x00; + CCAP2L = 0x11; + pca_module_pwm_set_duty_on_overflow(2, 0x20); + CHECK_EQ(CCAP2H, 0x20, "overflow update sets high byte"); + CHECK_EQ(CCAP2L, 0x11, "overflow update leaves low byte"); + + /* Enable/disable and status. */ + pca_module_pwm_disable(0); + CHECK_EQ(CCAPM0 & 0x02, 0x00, "pwm disabled clears PWMn"); + CHECK_EQ(is_pca_module_pwm_enabled(0), 0, "pwm status false after disable"); + pca_module_pwm_init(0, PCA_PWM_8BIT); + CHECK_EQ(is_pca_module_pwm_enabled(0), 1, "pwm status true after init"); +} + +int main(void) +{ + test_pin_group(); + test_clock_source(); + test_counter(); + test_run_and_idle(); + test_module_bits(); + test_compare_capture(); + test_pwm(); + + printf("pca: %d checks, %d failures\n", checks, failures); + return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/test/test_spi.c b/test/test_spi.c new file mode 100644 index 00000000..2cf2a2dc --- /dev/null +++ b/test/test_spi.c @@ -0,0 +1,268 @@ +/* + * Host tests for the SPI HAL (include/spi.h). + * + * These tests compile the real header against test/stubs/compiler.h and assert + * the exact register values the macros produce. They cover the parts of the + * module that are pure logic: bit masks, shifts, enum encodings and token + * pasting. They do not and cannot verify 8051 hardware behaviour (see the + * limitations noted in test/stubs/compiler.h). + * + * Expected values are derived from the STC15 series datasheet register tables: + * + * P_SW1 A2H S1_S1 S1_S0 CCP_S1 CCP_S0 SPI_S1 SPI_S0 0 DPS + * SPCTL CEH SSIG SPEN DORD MSTR CPOL CPHA SPR1 SPR0 + * SPSTAT CDH SPIF WCOL - - - - - - + * + * Two hardware behaviours the stub cannot model, and how the tests cope: + * + * - SPSTAT flags are cleared by writing 1 to them. The stub is plain memory, + * so bit_set(SPSTAT, mask) leaves the bits set rather than clearing them. + * The tests assert that the clear mask is written into SPSTAT, not that + * the bits read back as cleared. + * + * - Hardware raises SPIF when a transfer finishes. The stub never does, so + * the tests set SPIF by hand to model a completed transfer. The sync + * routines poll SPIF until it appears, so they cannot be exercised here + * at all; only the non blocking async routines are covered. + * + * Run: ctest --test-dir -V (see test/CMakeLists.txt) + */ + +#include +#include +#include + +#include +#include + +static int failures = 0; +static int checks = 0; + +#define CHECK_EQ(actual, expected, what) \ +do { \ + checks++; \ + unsigned long a_ = (unsigned long)(actual); \ + unsigned long e_ = (unsigned long)(expected); \ + if (a_ != e_) { \ + failures++; \ + printf("FAIL %s:%d %s: got 0x%02lX, expected 0x%02lX\n", \ + __FILE__, __LINE__, (what), a_, e_); \ + } \ +} while(0) + +/* ---------------------------------------------------------------- pin group */ + +static void test_pin_group(void) +{ + /* SPI_S1:SPI_S0 are P_SW1 bits 3:2. They sit BELOW the CCP bits (5:4) and + BELOW the UART1/S1 bits (7:6), so a group change must touch neither. + Bits 7:6 are S1_S1:S1_S0, not SPI: writing those silently moves UART1 + to different pins. */ + P_SW1 = 0xFF; + spi_set_pin_group(SPI_PIN_GROUP_1); /* 00 */ + CHECK_EQ(P_SW1, 0xF3, "group 1 clears SPI bits, keeps others"); + + P_SW1 = 0xFF; + spi_set_pin_group(SPI_PIN_GROUP_2); /* 01 */ + CHECK_EQ(P_SW1, 0xF7, "group 2 sets bit 2 only"); + + P_SW1 = 0xFF; + spi_set_pin_group(SPI_PIN_GROUP_3); /* 10 */ + CHECK_EQ(P_SW1, 0xFB, "group 3 sets bit 3 only"); + + /* From the power on reset value 0x40 (UART1 on P3.6/P3.7) UART1 must stay + put for every SPI group. */ + P_SW1 = 0x40; + spi_set_pin_group(SPI_PIN_GROUP_1); + CHECK_EQ(P_SW1, 0x40, "group 1 keeps UART1 on reset pins"); + + P_SW1 = 0x40; + spi_set_pin_group(SPI_PIN_GROUP_2); + CHECK_EQ(P_SW1, 0x44, "group 2 keeps UART1 on reset pins"); + + P_SW1 = 0x40; + spi_set_pin_group(SPI_PIN_GROUP_3); + CHECK_EQ(P_SW1, 0x48, "group 3 keeps UART1 on reset pins"); +} + +static void test_pin_group_round_trip(void) +{ + P_SW1 = 0x00; + spi_set_pin_group(SPI_PIN_GROUP_1); + CHECK_EQ(spi_get_pin_group(), SPI_PIN_GROUP_1, "get group 1"); + + P_SW1 = 0x00; + spi_set_pin_group(SPI_PIN_GROUP_2); + CHECK_EQ(spi_get_pin_group(), SPI_PIN_GROUP_2, "get group 2"); + + P_SW1 = 0x00; + spi_set_pin_group(SPI_PIN_GROUP_3); + CHECK_EQ(spi_get_pin_group(), SPI_PIN_GROUP_3, "get group 3"); +} + +static void test_pin_group_coexists_with_pca(void) +{ + /* SPI and CCP share P_SW1. Selecting one must not disturb the other. */ + P_SW1 = 0x00; + pca_set_pin_group(PCA_PIN_GROUP_3); /* bits 5:4 -> 0x20 */ + spi_set_pin_group(SPI_PIN_GROUP_2); /* bits 3:2 -> 0x04 */ + CHECK_EQ(P_SW1, 0x24, "PCA and SPI groups coexist"); + CHECK_EQ(pca_get_pin_group(), PCA_PIN_GROUP_3, "PCA group survives SPI change"); + CHECK_EQ(spi_get_pin_group(), SPI_PIN_GROUP_2, "SPI group survives PCA change"); +} + +/* ------------------------------------------------------------------- SPCTL */ + +static void test_init_master(void) +{ + /* SPEN | MSTR, then mode, clock, data order, and SSIG when CPHA=1. */ + spi_init_master(SPI_PIN_GROUP_1, SPI_MODE_0, SPI_CLOCK_DIV_16, false); + CHECK_EQ(SPCTL, 0x51, "master mode 0, /16, MSB first"); + + spi_init_master(SPI_PIN_GROUP_1, SPI_MODE_1, SPI_CLOCK_DIV_4, true); + CHECK_EQ(SPCTL, 0xF4, "master mode 1, /4, LSB first sets DORD and SSIG"); + + spi_init_master(SPI_PIN_GROUP_1, SPI_MODE_2, SPI_CLOCK_DIV_64, false); + CHECK_EQ(SPCTL, 0x5A, "master mode 2, /64, MSB first"); + + spi_init_master(SPI_PIN_GROUP_1, SPI_MODE_3, SPI_CLOCK_DIV_128, true); + CHECK_EQ(SPCTL, 0xFF, "master mode 3, /128, LSB first"); + + CHECK_EQ(is_spi_enabled(), 1, "master is enabled"); + CHECK_EQ(is_spi_master(), 1, "master reports master"); +} + +static void test_init_slave(void) +{ + /* SPEN only: no MSTR and no SSIG, the SS pin selects the device and the + clock divider is unused because the master clocks the bus. */ + spi_init_slave(SPI_PIN_GROUP_1, SPI_MODE_0, false); + CHECK_EQ(SPCTL, 0x40, "slave mode 0, MSB first"); + + spi_init_slave(SPI_PIN_GROUP_1, SPI_MODE_1, true); + CHECK_EQ(SPCTL, 0x64, "slave mode 1, LSB first"); + + spi_init_slave(SPI_PIN_GROUP_1, SPI_MODE_2, false); + CHECK_EQ(SPCTL, 0x48, "slave mode 2, MSB first"); + + spi_init_slave(SPI_PIN_GROUP_1, SPI_MODE_3, true); + CHECK_EQ(SPCTL, 0x6C, "slave mode 3, LSB first"); + + CHECK_EQ(is_spi_enabled(), 1, "slave is enabled"); + CHECK_EQ(is_spi_master(), 0, "slave reports slave"); +} + +static void test_destroy(void) +{ + spi_init_master(SPI_PIN_GROUP_1, SPI_MODE_1, SPI_CLOCK_DIV_4, false); + SPSTAT = 0x00; + spi_destroy(); + CHECK_EQ(SPCTL, 0x00, "destroy disables SPI"); + CHECK_EQ(SPSTAT, SPI_SPSTAT_CLEAR_MSK, "destroy writes the flag clear mask"); + CHECK_EQ(is_spi_enabled(), 0, "destroyed SPI reports disabled"); +} + +/* ------------------------------------------------------------- mode/clock */ + +static void test_get_mode_and_clock(void) +{ + SPCTL = 0x00; + CHECK_EQ(spi_get_mode(), SPI_MODE_0, "get mode 0"); + CHECK_EQ(spi_get_clock(), SPI_CLOCK_DIV_4, "get /4"); + + SPCTL = 0x0C; + CHECK_EQ(spi_get_mode(), SPI_MODE_3, "get mode 3 from CPOL|CPHA bits"); + + SPCTL = 0x03; + CHECK_EQ(spi_get_clock(), SPI_CLOCK_DIV_128, "get /128 from SPR bits"); + + /* Mode and clock occupy disjoint bits, so both survive together. */ + SPCTL = 0x0B; + CHECK_EQ(spi_get_mode(), SPI_MODE_2, "mode reads back with clock bits set"); + CHECK_EQ(spi_get_clock(), SPI_CLOCK_DIV_128, "clock reads back with mode bits set"); +} + +/* --------------------------------------------------------------- transfers */ + +static void test_async_transfer(void) +{ + SPCTL = SPI_SPEN_MSK | SPI_MSTR_MSK; + + SPSTAT = 0x00; + spi_async_transfer_start(0x7E); + CHECK_EQ(SPDAT, 0x7E, "async start loads the data register"); + CHECK_EQ(SPSTAT, SPI_SPSTAT_CLEAR_MSK, "async start writes the flag clear mask"); + + /* Model a transfer still in flight: hardware has not raised SPIF yet. */ + SPSTAT = 0x00; + CHECK_EQ(is_spi_async_transfer_complete(), 0, "not complete while SPIF clear"); + + /* Hardware raises SPIF when the byte has shifted out. */ + SPSTAT = SPI_SPIF_MSK; + CHECK_EQ(is_spi_async_transfer_complete(), 1, "complete when SPIF set"); + CHECK_EQ(spi_async_get_result(), 0x7E, "async result is the data register"); + + SPSTAT = 0x00; + spi_async_transfer_finish(); + CHECK_EQ(SPSTAT, SPI_SPSTAT_CLEAR_MSK, "finish writes the flag clear mask"); +} + +static void test_write_collision(void) +{ + SPSTAT = 0x00; + CHECK_EQ(is_spi_write_collision(), 0, "no collision when WCOL clear"); + + SPSTAT = SPI_WCOL_MSK; + CHECK_EQ(is_spi_write_collision(), 1, "collision when WCOL set"); + + spi_async_transfer_finish(); + CHECK_EQ(SPSTAT, SPI_WCOL_MSK | SPI_SPSTAT_CLEAR_MSK, + "finish writes the flag clear mask over WCOL"); +} + +/* ----------------------------------------------------------- slave select */ + +static void test_slave_select(void) +{ + P1 = 0xFF; + spi_ss_clr(SPI_PIN_GROUP_1); + CHECK_EQ(P1, 0xFB, "SS on P1.2 pulled low"); + + spi_ss_set(SPI_PIN_GROUP_1); + CHECK_EQ(P1, 0xFF, "SS on P1.2 pulled high"); + + P2 = 0xFF; + spi_ss_clr(SPI_PIN_GROUP_2); + CHECK_EQ(P2, 0xEF, "SS_2 on P2.4 pulled low"); + + spi_ss_set(SPI_PIN_GROUP_2); + CHECK_EQ(P2, 0xFF, "SS_2 on P2.4 pulled high"); + + P5 = 0xFF; + spi_ss_clr(SPI_PIN_GROUP_3); + CHECK_EQ(P5, 0xEF, "SS_3 on P5.4 pulled low"); + + spi_ss_set(SPI_PIN_GROUP_3); + CHECK_EQ(P5, 0xFF, "SS_3 on P5.4 pulled high"); +} + +/* --------------------------------------------------------------------- main */ + +int main(void) +{ + test_pin_group(); + test_pin_group_round_trip(); + test_pin_group_coexists_with_pca(); + test_init_master(); + test_init_slave(); + test_destroy(); + test_get_mode_and_clock(); + test_async_transfer(); + test_write_collision(); + test_slave_select(); + + printf("%s: %d check(s), %d failure(s)\n", + failures ? "FAILED" : "ALL PASSED", checks, failures); + + return failures != 0; +}