diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1646525 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +.pio +cmake-build-teensy40 +CMakeListsPrivate.txt diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a8bbc57 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,67 @@ +# Continuous Integration (CI) is the practice, in software +# engineering, of merging all developer working copies with a shared mainline +# several times a day < https://docs.platformio.org/page/ci/index.html > +# +# Documentation: +# +# * Travis CI Embedded Builds with PlatformIO +# < https://docs.travis-ci.com/user/integration/platformio/ > +# +# * PlatformIO integration with Travis CI +# < https://docs.platformio.org/page/ci/travis.html > +# +# * User Guide for `platformio ci` command +# < https://docs.platformio.org/page/userguide/cmd_ci.html > +# +# +# Please choose one of the following templates (proposed below) and uncomment +# it (remove "# " before each line) or use own configuration according to the +# Travis CI documentation (see above). +# + + +# +# Template #1: General project. Test it using existing `platformio.ini`. +# + +# language: python +# python: +# - "2.7" +# +# sudo: false +# cache: +# directories: +# - "~/.platformio" +# +# install: +# - pip install -U platformio +# - platformio update +# +# script: +# - platformio run + + +# +# Template #2: The project is intended to be used as a library with examples. +# + +# language: python +# python: +# - "2.7" +# +# sudo: false +# cache: +# directories: +# - "~/.platformio" +# +# env: +# - PLATFORMIO_CI_SRC=path/to/test/file.c +# - PLATFORMIO_CI_SRC=examples/file.ino +# - PLATFORMIO_CI_SRC=path/to/test/directory +# +# install: +# - pip install -U platformio +# - platformio update +# +# script: +# - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6b253ed --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,83 @@ +# !!! WARNING !!! AUTO-GENERATED FILE, PLEASE DO NOT MODIFY IT AND USE +# https://docs.platformio.org/page/projectconf/section_env_build.html#build-flags +# +# If you need to override existing CMake configuration or add extra, +# please create `CMakeListsUser.txt` in the root of project. +# The `CMakeListsUser.txt` will not be overwritten by PlatformIO. + +cmake_minimum_required(VERSION 3.2) +project(teensy4_i2c) + +include(CMakeListsPrivate.txt) + +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/CMakeListsUser.txt) +include(CMakeListsUser.txt) +endif() + +add_custom_target( + PLATFORMIO_BUILD ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion run "$<$>:-e${CMAKE_BUILD_TYPE}>" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target( + PLATFORMIO_BUILD_VERBOSE ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion run --verbose "$<$>:-e${CMAKE_BUILD_TYPE}>" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target( + PLATFORMIO_UPLOAD ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion run --target upload "$<$>:-e${CMAKE_BUILD_TYPE}>" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target( + PLATFORMIO_CLEAN ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion run --target clean "$<$>:-e${CMAKE_BUILD_TYPE}>" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target( + PLATFORMIO_MONITOR ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion device monitor "$<$>:-e${CMAKE_BUILD_TYPE}>" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target( + PLATFORMIO_TEST ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion test "$<$>:-e${CMAKE_BUILD_TYPE}>" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target( + PLATFORMIO_PROGRAM ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion run --target program "$<$>:-e${CMAKE_BUILD_TYPE}>" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target( + PLATFORMIO_UPLOADFS ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion run --target uploadfs "$<$>:-e${CMAKE_BUILD_TYPE}>" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target( + PLATFORMIO_UPDATE_ALL ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion update + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target( + PLATFORMIO_REBUILD_PROJECT_INDEX ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion init --ide clion + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target( + PLATFORMIO_DEVICE_LIST ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion device list + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_executable(${PROJECT_NAME} ${SRC_LIST}) diff --git a/CMakeListsUser.txt b/CMakeListsUser.txt new file mode 100644 index 0000000..7a89621 --- /dev/null +++ b/CMakeListsUser.txt @@ -0,0 +1,3 @@ +# Make tests visible to CLion. +# Use CLion's "Mark directory as" option to mark the "test" directory as a source folder. +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/test/test_register_i2c) diff --git a/LICENSE b/LICENSE deleted file mode 100644 index fcf80cb..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Richard Gemmell - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index e4c8b09..6de74c5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,90 @@ -# teensy4_i2c -An I2C library for the Teensy 4. Provides slave and master mode. +#teensy4_i2c +An I2C library for the [Teensy 4](https://www.pjrc.com/store/teensy40.html) +microcontroller. + +The Teensy 4.0 uses the [NXP i.MXRT 1062](https://www.nxp.com/products/processors-and-microcontrollers/arm-microcontrollers/i.mx-rt-series/i.mx-rt1060-crossover-processor-with-arm-cortex-m7-core:i.MX-RT1060) +microcontroller with n ARM Corex-M7 core. + +This library is intended to be used as a drop in replacement for the +Wire library in [Teensyduino](https://www.pjrc.com/teensy/td_download.html). +The primary reason for writing it was to add support for Slave mode. + +The driver implementations IMX_RT1060_I2CMaster and IMX_RT1060_I2CSlave have +no dependencies on Arduino itself and relatively few dependencies on the +Teensy code. This means that is should be possible to use the drivers with +other tool sets without having to change too much. + +##Usage +### Use I2C Register Wrappers +I2CDevice and I2CRegisterSlave classes make it very simple to follow +the standard I2C pattern of reading or writing to "registers". +I recommend that you use this interface if it's suitable. + +1. Download the code and put it in your include path. +1. #include "i2c_device.h" if you have a master and wish +to read from a slave device. +1. #include "i2c_register_slave.h" if you want to implement +a slave device to be read by a master. +1. See the examples in the examples/simple directory + +### Use the Driver Directly +The driver interfaces are defined in i2c_driver.h. These provide +everything you need to use I2C without the limitations of the Wire +library. The key classes are I2CMaster and I2CSlave. + +1. Download the code and put it in your include path. +1. #include "imx_rt1060_i2c_driver.h" +1. See the examples in the examples/raw directory + +###Replacing Wire.h +Follow these instructions if you have already written code to +use Wire.h and don't want to change it. I don't recommend using +the Wire API unless you have to. See below for alternatives. +1. Download the code and put it in your include path. +1. Change all #includes from Wire.h to i2c_driver_wire.h. +1. If any of your dependencies use Wire.h you'll have to +modify them to use i2c_driver_wire.h instead. +1. See the examples in the examples/wire directory + +##Features +* Supports all features that are required by the I2C specification +for master, multi-master and slave devices. +* Drop in replacement for the Wire library +* Master Mode +* Slave Mode +* Standard Mode (100 kbps) +* Fast Mode (400 kbps) +* Fast Mode Plus (1 Mbps) +* Multi-master support +* Clock stretching in Slave Mode +* Non-blocking API for Master Mode and Slave Mode +* Comprehensive error handling + +## Not Tested +I haven't been able to test some features because of hardware and time +restrictions. These features *should* work but don't be surprised if +they don't. Please contact me if you encounter any problems. +* Multi-master configurations +* Noisy environments +* Cortex i.MX RT1050 + +## Not Implemented +The following features are supported by the NXP i.MXRT 1062 processor but +I haven't implemented them in this driver. +Please contact me if you need any of these features. +* Alternative pins for port 1 +* Direct Memory Access (DMA) +* High Speed Mode (3.4 Mbps) +* Ultra Fast Mode (5 Mbps) +* Multiple addresses for a single slave +* 10 bit slave addresses +* Glitch filter (for slave mode) +* SMBus Alert +* General Call +* 4 pin I2C in Master mode +* Master reading more than 256 bytes in a single transfer + +## Version History +| Version | Release Date | Comment | +| ------- |-------------------| ----------------| +| v0.9.0 | 7th November 2019 | Initial Version | \ No newline at end of file diff --git a/examples/raw/raw_master_reader/raw_master_reader.ino b/examples/raw/raw_master_reader/raw_master_reader.ino new file mode 100644 index 0000000..a565821 --- /dev/null +++ b/examples/raw/raw_master_reader/raw_master_reader.ino @@ -0,0 +1,119 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) + +// This example WILL NOT work unless you have an INA260 +// current sensor connected to pins 16 and 17. +// +// Demonstrates use of the raw I2C driver. +// Creates an I2C master and reads a device with it. +// +// This is an advanced example. Use the "simple" examples +// instead if you want to follow the typical usage pattern +// for I2C. + +#include +#include +#include "imx_rt1060/imx_rt1060_i2c_driver.h" + +// Blink the LED to make sure the Teensy hasn't hung +IntervalTimer blink_timer; +volatile bool led_high = false; +void blink_isr(); + +// The slave is an INA 260 current sensor +const uint16_t slave_address = 0x40; +const uint8_t manufacturer_id_register = 0xFE; +const uint8_t die_id_register = 0xFF; +I2CMaster& master = Master1; + +// Create a buffer to receive data from the slave. +uint8_t rx_buffer[2] = {}; + +void finish(); +bool ok(const char* message); +uint16_t get_int_from_buffer(); + +void setup() { + // Turn the LED on + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, true); + + // Create a timer to blink the LED + blink_timer.begin(blink_isr, 500000); + + // Initialise the master + master.begin(100 * 1000U); + + // Enable the serial port for debugging + Serial.begin(9600); + Serial.println("Started"); +} + +void loop() { + Serial.println(""); + // Request the Manufacturer ID + master.write_async(slave_address, (uint8_t*)&manufacturer_id_register, sizeof(manufacturer_id_register), false); + finish(); + if(ok("Failed to send manufacture id register value")) { + master.read_async(slave_address, rx_buffer, sizeof(rx_buffer), true); + finish(); + if (ok("Failed to read manufacture ID.")) { + uint16_t manufacturerID = get_int_from_buffer(); + const uint16_t expected = 0x5449; + if (manufacturerID == expected) { + Serial.println("Got correct Manufacturer ID."); + } else { + Serial.printf("Manufacturer ID is 0x%X. Expected 0x%X.\n", manufacturerID, expected); + } + } + } + + // Request the Die ID + master.write_async(slave_address, (uint8_t*)&die_id_register, sizeof(die_id_register), false); + finish(); + if(ok("Failed to send die id register value")) { + master.read_async(slave_address, rx_buffer, sizeof(rx_buffer), true); + finish(); + if (ok("Failed to read die ID.")) { + uint16_t dieId = get_int_from_buffer(); + const uint16_t expected = 0x2270; + if (dieId == expected) { + Serial.println("Got correct Die ID."); + } else { + Serial.printf("Die ID is 0x%X. Expected 0x%X.\n", dieId, expected); + } + } + } + + delay(1000); +} + +uint16_t get_int_from_buffer() { + uint16_t result = ((uint16_t)rx_buffer[0] << 8U) + ((uint16_t)rx_buffer[1]); + return result; +} + +bool ok(const char* message) { + if (master.has_error()) { + Serial.print(message); + Serial.print(" Error: "); + Serial.println((int)master.error()); + return false; + } + return true; +} + +void finish() { + elapsedMillis timeout; + while (timeout < 200) { + if (master.finished()){ + return; + } + } + Serial.println("Master: ERROR timed out waiting for transfer to finish."); +} + +void blink_isr() { + led_high = !led_high; + digitalWrite(LED_BUILTIN, led_high); +} diff --git a/examples/raw/raw_simple_sensor/raw_simple_sensor.ino b/examples/raw/raw_simple_sensor/raw_simple_sensor.ino new file mode 100644 index 0000000..1eacc6f --- /dev/null +++ b/examples/raw/raw_simple_sensor/raw_simple_sensor.ino @@ -0,0 +1,110 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) + +// A Simple Sensor +// Demonstrates use of the raw I2C driver as a simple slave transmitter. +// The sensor provides the master with a simulated temperature reading +// on demand. +// To use it, connect a master to the Teensy on pins 18 and 19. +// Send read requests to the Teensy. +// +// This is an advanced example. Use the "simple" examples +// instead if you want to follow the typical usage pattern +// for I2C. + +#include +#include +#include "imx_rt1060/imx_rt1060_i2c_driver.h" + +// Blink the LED to make sure the Teensy hasn't hung +IntervalTimer blink_timer; +volatile bool led_high = false; +void blink_isr(); + +const uint16_t slave_address = 0x002D; +I2CSlave& slave = Slave; +void before_transmit_isr(); +void after_transmit(); + +// Set up transmit buffer and temperature value +const size_t tx_buffer_size = 2; +uint8_t tx_buffer[tx_buffer_size] = {}; +uint16_t fake_temp = 1; + +// Flags for handling callbacks +volatile bool before_transmit_received = false; +volatile bool after_transmit_received = false; +volatile bool buffer_underflow_detected = false; +void log_transmit_events(); + + +void setup() { + // Turn the LED on + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, true); + + // Create a timer to blink the LED + blink_timer.begin(blink_isr, 500 * 1000); + + // Configure I2C Slave and Start It + slave.before_transmit(before_transmit_isr); + slave.after_transmit(after_transmit); + slave.set_transmit_buffer(tx_buffer, tx_buffer_size); + slave.listen(slave_address); + + // Enable the serial port for debugging + Serial.begin(9600); + Serial.println("Started"); +} + +void loop() { + // Work out the current temperature and make it available to the master + fake_temp++; + memcpy(tx_buffer, &fake_temp, sizeof(fake_temp)); + + // Now we've got the time, we can handle the results of the ISR callbacks. + log_transmit_events(); + + // The master can read the sensor as often as it likes while we sleep + delay(2); +} + +// Called by an interrupt service routine. +// This function must be _very_ fast. Avoid IO. +void before_transmit_isr() { + before_transmit_received = true; +} + +// Called by an interrupt service routine. +// This function must be _very_ fast. Avoid IO. +void after_transmit() { + after_transmit_received = true; + if (slave.has_error()) { + I2CError error = slave.error(); + if (error == I2CError::buffer_underflow) { + buffer_underflow_detected = true; + } else { + Serial.println("Unexpected error"); + } + } +} + +void log_transmit_events() { + if (before_transmit_received) { + Serial.println("App: Transmission started."); + before_transmit_received = false; + } + if (after_transmit_received) { + Serial.println("App: Transmission complete."); + after_transmit_received = false; + if (buffer_underflow_detected) { + Serial.println("App: Buffer Underflow. (Master asked for too many bytes.)"); + buffer_underflow_detected = false; + } + } +} + +void blink_isr() { + led_high = !led_high; + digitalWrite(LED_BUILTIN, led_high); +} diff --git a/examples/raw/raw_slave_receiver/raw_slave_receiver.ino b/examples/raw/raw_slave_receiver/raw_slave_receiver.ino new file mode 100644 index 0000000..b5a3cbf --- /dev/null +++ b/examples/raw/raw_slave_receiver/raw_slave_receiver.ino @@ -0,0 +1,106 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) + +// Demonstrates use of the raw I2C driver as a slave receiver. +// Receives data from a master device. +// +// To use it, connect a master to the Teensy on pins 18 and 19. +// Use the master to write to the Teensy. +// +// This is an advanced example. Use the "simple" examples +// instead if you want to follow the typical usage pattern +// for I2C. + +#include +#include +#include "imx_rt1060/imx_rt1060_i2c_driver.h" + +// Blink the LED to make sure the Teensy hasn't hung +IntervalTimer blink_timer; +volatile bool led_high = false; +void blink_isr(); + +const uint16_t slave_address = 0x002D; +I2CSlave& slave = Slave; +void after_receive(int size); + +// Double receive buffers to hold data from master. +const size_t slave_rx_buffer_size = 4; +uint8_t slave_rx_buffer[slave_rx_buffer_size] = {}; +uint8_t slave_rx_buffer_2[slave_rx_buffer_size] = {}; +volatile size_t slave_bytes_received = 0; + +void log_message_received(); + +void setup() { + // Turn the LED on + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, true); + + // Create a timer to blink the LED + blink_timer.begin(blink_isr, 500000); + + // Configure I2C Slave + slave.after_receive(after_receive); + slave.set_receive_buffer(slave_rx_buffer, slave_rx_buffer_size); + + // Enable the slave + slave.listen(slave_address); + + // Enable the serial port for debugging + Serial.begin(9600); + Serial.println("Started"); +} + +void loop() { + if (slave_bytes_received) { + // Handle the message. + log_message_received(); + + // Clear slave_bytes_received to signal that we're ready for another message + slave_bytes_received = 0; + } + + // We could receive multiple message while we're asleep. + // This example is modelling an application where it's Ok + // to drop messages. + delay(1); +} + +// Called by the I2C interrupt service routine. +// This method must be as fast as possible. +// Do not perform IO in it. +void after_receive(int size) { + // This is the only time we can guarantee that the + // receive buffer is not changing. + // Copy the content so we can handle it in the main loop. + if (!slave_bytes_received) { + memcpy(slave_rx_buffer_2, slave_rx_buffer, size); + slave_bytes_received = size; + } + // else ignore this message because the main loop hasn't + // handled the previous one yet. +} + +void log_message_received() { + if (slave.has_error()) { + if (slave.error() == I2CError::buffer_overflow) { + Serial.println("App: Buffer Overflow. (Master sent too many bytes.)"); + } else { + Serial.println("App: Unexpected error"); + } + } + Serial.print("App: Slave received "); + Serial.print(slave_bytes_received); + Serial.print(" bytes: "); + for(size_t i=0; i +#include + +// Blink the LED to make sure the Teensy hasn't hung +IntervalTimer blink_timer; +volatile bool led_high = false; + +// The slave is an INA 260 current sensor +const uint16_t slave_address = 0x40; +const uint8_t manufacturer_id_register = 0xFE; +const uint16_t expected_manufacturer_id = 0x5449; +const uint8_t die_id_register = 0xFF; +const uint16_t expected_die_id = 0x2270; +bool configured = false; +I2CDevice sensor = I2CDevice(Master1, slave_address, _BIG_ENDIAN); + + +bool configure_sensor(); +void report_error(const char* message); + +void setup() { + pinMode(LED_BUILTIN, OUTPUT); + + // Initialise the master + Master1.begin(400 * 1000U); + + // Enable the serial port for debugging + Serial.begin(9600); + Serial.println("Started"); + + // Check that we can see the sensor and configure it. + configured = configure_sensor(); +} + +void loop() { + if (configured) { + int16_t current = 0; + if (!sensor.read(0x01, ¤t, true)) { + report_error("ERROR: Failed to read current"); + } + Serial.printf("Current: %.0f mA\n", current * 1.25); + + int16_t voltage = 0; + if (!sensor.read(0x02, &voltage, true)) { + report_error("ERROR: Failed to read voltage"); + } + Serial.printf("Voltage: %.0f mV\n", ((float)voltage) * 1.25); + } else { + Serial.println("Not configured"); + } + + // Blink the LED + digitalWrite(LED_BUILTIN, HIGH); + delay(900); + digitalWrite(LED_BUILTIN, LOW); + delay(500); +} + +bool configure_sensor() { + // Check the manufacturer ID and check for I2C errors + uint16_t manufacturerId; + if (sensor.read(manufacturer_id_register, &manufacturerId, false)) { + if (manufacturerId != expected_manufacturer_id) { + Serial.printf("ERROR: Manufacturer ID is 0x%X. Expected 0x%X.\n", manufacturerId, expected_manufacturer_id); + return false; + } + } else { + report_error("ERROR: Failed to send manufacturer id register value"); + return false; + } + + // Check the Die ID without checking for I2C errors + uint16_t dieId = 0; + sensor.read(die_id_register, &dieId, false); + if (dieId != expected_die_id) { + Serial.printf("ERROR: Die ID is 0x%X. Expected 0x%X.\n", dieId, expected_die_id); + return false; + } + + // Configure it + uint16_t new_config = 0x6127U | 0x100U | 0x80U | 0x18U; + if (!sensor.write(0x00, new_config, false)) { + report_error("ERROR: Failed to set configuration register"); + return false; + } + if (!sensor.write(0x06, (uint16_t)0x0403, true)) { + report_error("ERROR: Failed to set Mask/Enable Register"); + return false; + } + Serial.println("Configured sensor successfully."); + return true; +} + +void report_error(const char* message) { + Serial.print(message); + Serial.print(" Error: "); + Serial.println((int)Master1.error()); +} diff --git a/examples/simple/i2c_register_slave/i2c_register_slave.ino b/examples/simple/i2c_register_slave/i2c_register_slave.ino new file mode 100644 index 0000000..ad3a468 --- /dev/null +++ b/examples/simple/i2c_register_slave/i2c_register_slave.ino @@ -0,0 +1,101 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) + +// Demonstrates use of the I2C Device class to represent a slave device. +// Creates an I2C master, configures a device and reads registers. +// +// To use it, connect a master to the Teensy on pins 18 and 19. +// Use the master to read registers 0 to 10. +// The master can write registers 0 and 1 to change the output. +// loop() updates the output values. Changes to Settings will +// be applied the next time loop() runs. +// +// I recommend this approach for using the Teensy as an I2C slave. + +#include +#include + +volatile bool led_high = false; + +// Registers that the caller can both read and write +struct Settings { + int8_t temp_offset; // Zero point for temperature. Default -40. + int8_t scaling; // Writable. Sets scaling of voltage and current fields. +}; + +// Registers that the caller can only read +struct Registers { + uint8_t flags = 0; // bit 0 : new data available + uint8_t temp = 0; // degrees C starting at temp_offset + uint16_t reserved = 0; // Fill up to the next word boundary + int32_t voltage = 0; // in 10ths of a volt + int32_t current = 0; // in 10ths of an amp +}; + +Settings settings = {-40, 10}; +Registers registers; +I2CRegisterSlave registerSlave = I2CRegisterSlave(Slave, (uint8_t*)&settings, sizeof(Settings), (uint8_t*)®isters, sizeof(Registers)); + +// The code to read the raw data might be too expensive to +// call in an interrupt service routine. It's represented +// by these dummy methods. +int8_t get_temp(); +int32_t get_current(); +int32_t get_voltage(); + +// Callbacks. +void on_read_isr(uint8_t reg_num); + +void setup() { + pinMode(LED_BUILTIN, OUTPUT); + + // Start listening + registerSlave.listen(0x2D); + registerSlave.after_read(on_read_isr); + + // Enable the serial port for debugging + Serial.begin(9600); + Serial.println("Registers. 0 => temp_offset, 1 => scaling, 2 => flags, 3 => temp, 6 => voltage, 10 => current."); +} + +void loop() { + // Gather raw data and convert to output values + Registers new_values; + int16_t raw_temp = get_temp(); + new_values.temp = raw_temp - (int16_t)settings.temp_offset; + int32_t raw_voltage = get_voltage(); + new_values.voltage = raw_voltage * settings.scaling; + int32_t raw_current = get_current(); + new_values.current = raw_current * settings.scaling; + + // Block copy new values over the top of the old values + // and then set the "new data" bit. + memcpy(®isters, &new_values, sizeof(Registers)); + registers.flags = 1; + + // Blink the LED + digitalWrite(LED_BUILTIN, led_high); + led_high = !led_high; + delay(200); // Update at 5 Hz +} + +void on_read_isr(uint8_t reg_num) { + // Clear the "new data" bit so the master knows it's + // already read this set of values. + registers.flags = 0; +} + +// Gets temperature in degrees +int8_t get_temp() { + return 21; +} + +// Gets current in milliamps +int32_t get_current() { + return 1250; +} + +// Gets voltage in millivolts +int32_t get_voltage() { + return 12000; +} diff --git a/examples/wire/master_reader/master_reader.ino b/examples/wire/master_reader/master_reader.ino new file mode 100644 index 0000000..68c9ce0 --- /dev/null +++ b/examples/wire/master_reader/master_reader.ino @@ -0,0 +1,50 @@ +// Wire Master Reader +// by Nicholas Zambetti +// Modified by Richard Gemmell Oct 2019 + +// Demonstrates use of the Wire library +// Reads data from an I2C/TWI slave device connected to pins 16 and 17. +// +// Consider using the I2CDevice class instead of Wire to read a sensor. + +// Created 29 March 2006 + +// This example code is in the public domain. + + +#include +#include +#include + +int led = LED_BUILTIN; + +void setup() +{ + pinMode(led, OUTPUT); + Wire1.begin(); // join i2c bus + Serial.begin(9600); // start serial for output +} + +void loop() +{ + Serial.print("read: "); + + digitalWrite(led, HIGH); // briefly flash the LED + Wire1.requestFrom(0x40, 2); // request 2 bytes from slave device #64 + + // Can peek at the first byte + if (Wire1.available()) { + Serial.print((char)Wire1.peek()); + Serial.print(" "); + } + + while(Wire1.available()) { // slave may send less than requested + char c = Wire1.read(); // receive a byte as character + Serial.print(c); // print the character + } + + Serial.println(); + delay(500); + digitalWrite(led, LOW); + delay(500); +} diff --git a/examples/wire/read_sensor/read_sensor.ino b/examples/wire/read_sensor/read_sensor.ino new file mode 100644 index 0000000..d523675 --- /dev/null +++ b/examples/wire/read_sensor/read_sensor.ino @@ -0,0 +1,57 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) + +// A Simple Sensor +// Shows how to use a typical I2C sensor. +// The program configures the sensor and then reads +// from a register. This is a common pattern. +// To use it, connect a master to the Teensy on pins 18 and 19. +// +// Consider using the I2CDevice class instead of Wire to read a sensor. + +#include +#include +#include + +int sensor_address = 0x40; + +void setup() +{ + pinMode(LED_BUILTIN, OUTPUT); + Serial.begin(9600); // start serial for output + + Wire1.setClock(400 * 1000); // Set the clock speed before calling begin() + Wire1.begin(); // join i2c bus + + // Configure the slave by writing to register 0 + uint16_t new_config = 0x6763; // Suitable configuration values + Serial.println(new_config, HEX); + Wire1.beginTransmission(sensor_address); + Wire1.write((uint8_t)0); // Write to register 0 + Wire1.write((uint8_t*)&new_config, sizeof(new_config)); // Send the new config + Wire1.endTransmission(true); +} + +void loop() +{ + digitalWrite(LED_BUILTIN, HIGH); // briefly flash the LED + + // Read back the config + + // Request the contents of register 0 + Wire1.beginTransmission(sensor_address); + Wire1.write((uint8_t)0); + Wire1.endTransmission(false); + + // Read the register + Wire1.requestFrom(sensor_address, 2, true); + while(Wire1.available()) { + uint8_t c = Wire1.read(); + Serial.print(c, HEX); // Note that the byte order has changed! + } + + Serial.println(); + delay(500); + digitalWrite(LED_BUILTIN, LOW); + delay(500); +} diff --git a/examples/wire/slave_receiver/slave_receiver.ino b/examples/wire/slave_receiver/slave_receiver.ino new file mode 100644 index 0000000..107b705 --- /dev/null +++ b/examples/wire/slave_receiver/slave_receiver.ino @@ -0,0 +1,51 @@ +// Wire Slave Receiver +// by Nicholas Zambetti +// Modified by Richard Gemmell Oct 2019 + +// Demonstrates use of the Wire library +// Receives data as an I2C/TWI slave device +// Refer to the "Wire Master Writer" example for use with this +// To use it, connect a master to the Teensy on pins 18 and 19. +// +// Consider using the I2CRegisterSlave class instead of Wire to +// create an I2C slave. + +// Created 29 March 2006 + +// This example code is in the public domain. + +#include +#include +#include + +void receiveEvent(int howMany); + +int led = LED_BUILTIN; + +void setup() +{ + pinMode(led, OUTPUT); + Wire.begin(9); // join i2c bus with address #9 + Wire.onReceive(receiveEvent); // register event + Serial.begin(9600); // start serial for output +} + +void loop() +{ + delay(100); +} + +// function that executes whenever data is received from master +// this function is registered as an event, see setup() +void receiveEvent(int howMany) +{ + digitalWrite(led, HIGH); // briefly flash the LED + while(Wire.available() > 1) { // loop through all but the last + char c = Wire.read(); // receive byte as a character + Serial.print(c); // print the character + } + Serial.println(); + int x = Wire.read(); // receive byte as an integer + Serial.println(x); // print the integer + digitalWrite(led, LOW); +} diff --git a/examples/wire/slave_sender/slave_sender.ino b/examples/wire/slave_sender/slave_sender.ino new file mode 100644 index 0000000..ef06547 --- /dev/null +++ b/examples/wire/slave_sender/slave_sender.ino @@ -0,0 +1,45 @@ +// Wire Slave Sender +// by Nicholas Zambetti +// Modified by Richard Gemmell Oct 2019 + +// Demonstrates use of the Wire library +// Sends data as an I2C/TWI slave device +// Refer to the "Wire Master Reader" example for use with this +// To use it, connect a master to the Teensy on pins 18 and 19. +// +// Consider using the I2CRegisterSlave class instead of Wire to +// create an I2C slave. + +// Created 29 March 2006 + +// This example code is in the public domain. + +#include +#include +#include + +void requestEvent(); + +int led = LED_BUILTIN; + +void setup() +{ + pinMode(led, OUTPUT); + Wire.begin(8); // join i2c bus with address #8 + Wire.onRequest(requestEvent); // register event +} + +void loop() +{ + delay(100); +} + +// function that executes whenever data is requested by master +// this function is registered as an event, see setup() +void requestEvent() +{ + digitalWrite(led, HIGH); // briefly flash the LED + Wire.write("hello "); // respond with message of 6 bytes + // as expected by master + digitalWrite(led, LOW); +} diff --git a/include/README b/include/README new file mode 100644 index 0000000..45496b1 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/init.bat b/init.bat new file mode 100644 index 0000000..a087cdf --- /dev/null +++ b/init.bat @@ -0,0 +1,3 @@ +REM Reinitialises the project by rebuilding the make files. +REM You must run this after adding new files and directories. +pio init --ide=clion --board=teensy40 \ No newline at end of file diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..8c9c29c --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..abb48b1 --- /dev/null +++ b/library.properties @@ -0,0 +1,9 @@ +name=Teensy4 I2C +version=0.9.0 +author=Richard Gemmell +maintainer=Richard Gemmell +sentence=An I2C driver for the Teensy 4.0. +paragraph=This is a drop in replacement for the standard Wire library for I2C. It supports slave and master modes. The I2CDevice and I2CRegisterSlave classes simplify the most common I2C usage. +category=Communication +url=https://github.com/Richard-Gemmell/teensy4_i2c +architectures=* diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..aa74ab3 --- /dev/null +++ b/license.txt @@ -0,0 +1,19 @@ +Copyright © 2019 Richard Gemmell + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..88c6786 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,15 @@ +;PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:teensy40] +platform = teensy +board = teensy40 +framework = arduino +test_build_project_src = true diff --git a/src/i2c_device.h b/src/i2c_device.h new file mode 100644 index 0000000..e402d07 --- /dev/null +++ b/src/i2c_device.h @@ -0,0 +1,171 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) + +#ifndef I2C_DEVICE_H +#define I2C_DEVICE_H + +#include +#include "i2c_driver.h" +#ifdef __IMXRT1062__ +#include "imx_rt1060/imx_rt1060_i2c_driver.h" +#endif + +// Wraps I2CMaster to make it easy to configure a typical I2C device and then read +// "registers". All calls block so this API should not be called within interrupt +// service routines. +class II2CDevice { +public: + virtual bool write(uint8_t reg, uint8_t* buffer, size_t num_bytes, bool send_stop) = 0; + virtual bool write(uint8_t reg, uint8_t value, bool send_stop) = 0; + virtual bool write(uint8_t reg, int8_t value, bool send_stop) = 0; + virtual bool write(uint8_t reg, uint16_t value, bool send_stop) = 0; + virtual bool write(uint8_t reg, int16_t value, bool send_stop) = 0; + virtual bool write(uint8_t reg, uint32_t value, bool send_stop) = 0; + virtual bool write(uint8_t reg, int32_t value, bool send_stop) = 0; + + virtual bool read(uint8_t reg, uint8_t* buffer, size_t num_bytes, bool send_stop) = 0; + virtual bool read(uint8_t reg, uint8_t* value, bool send_stop) = 0; + virtual bool read(uint8_t reg, int8_t* value, bool send_stop) = 0; + virtual bool read(uint8_t reg, uint16_t* value, bool send_stop) = 0; + virtual bool read(uint8_t reg, int16_t* value, bool send_stop) = 0; + virtual bool read(uint8_t reg, uint32_t* value, bool send_stop) = 0; + virtual bool read(uint8_t reg, int32_t* value, bool send_stop) = 0; +}; + +// Wraps I2CMaster to make it easy to configure a typical I2C device and then read +// "registers". All calls block so this API should not be called within interrupt +// service routines. +class I2CDevice : public II2CDevice { +public: + const uint32_t timeout_millis = 200; + + I2CDevice(I2CMaster& master, uint16_t address, int device_byte_order = _LITTLE_ENDIAN) + : master(master), address(address) { + swap_bytes = _BYTE_ORDER != device_byte_order; + } + + bool write(uint8_t reg, uint8_t* buffer, size_t num_bytes, bool send_stop) override { + uint8_t big_buffer[num_bytes+1]; + big_buffer[0] = reg; + memcpy(big_buffer+1, buffer, num_bytes); + master.write_async(address, big_buffer, sizeof(big_buffer), send_stop); + finish(); + return !master.has_error(); + } + + inline bool write(uint8_t reg, uint8_t value, bool send_stop) override { + return write(reg, &value, 1, send_stop); + } + + inline bool write(uint8_t reg, int8_t value, bool send_stop) override { + return write(reg, (uint8_t*)&value, 1, send_stop); + } + + inline bool write(uint8_t reg, uint16_t value, bool send_stop) override { + if (swap_bytes) { + uint16_t swapped = __builtin_bswap16(value); + return write(reg, (uint8_t*)&swapped, 2, send_stop); + } else { + return write(reg, (uint8_t*)&value, 2, send_stop); + } + } + + inline bool write(uint8_t reg, int16_t value, bool send_stop) override { + if (swap_bytes) { + int16_t swapped = __builtin_bswap16(value); + return write(reg, (uint8_t*)&swapped, 2, send_stop); + } else { + return write(reg, (uint8_t*)&value, 2, send_stop); + } + } + + inline bool write(uint8_t reg, uint32_t value, bool send_stop) override { + if (swap_bytes) { + uint32_t swapped = __builtin_bswap32(value); + return write(reg, (uint8_t*)&swapped, 4, send_stop); + } else { + return write(reg, (uint8_t*)&value, 4, send_stop); + } + } + + inline bool write(uint8_t reg, int32_t value, bool send_stop) override { + if (swap_bytes) { + int32_t swapped = __builtin_bswap32(value); + return write(reg, (uint8_t*)&swapped, 4, send_stop); + } else { + return write(reg, (uint8_t*)&value, 4, send_stop); + } + } + + bool read(uint8_t reg, uint8_t* buffer, size_t num_bytes, bool send_stop) override { + bool has_error = true; + if (write_register(reg)) { + master.read_async(address, buffer, num_bytes, send_stop); + finish(); + has_error = master.has_error(); + } + if (has_error) { + // Zero the buffer if the read failed to avoid using stale data. + memset(buffer, 0, num_bytes); + } + return !has_error; + } + + inline bool read(uint8_t reg, uint8_t* value, bool send_stop) override { + return read(reg, value, 1, send_stop); + } + + inline bool read(uint8_t reg, int8_t* value, bool send_stop) override { + return read(reg, (uint8_t*)value, 1, send_stop); + } + + inline bool read(uint8_t reg, uint16_t* value, bool send_stop) override { + if (read(reg, (uint8_t*)value, 2, send_stop)) { + if (swap_bytes) { + *value = __builtin_bswap16(*value); + } + return true; + } + return false; + } + + inline bool read(uint8_t reg, int16_t* value, bool send_stop) override { + return read(reg, (uint16_t*)value, send_stop); + } + + inline bool read(uint8_t reg, uint32_t* value, bool send_stop) override { + if (read(reg, (uint8_t*)value, 4, send_stop)) { + if (swap_bytes) { + *value = __builtin_bswap32(*value); + } + return true; + } + return false; + } + + inline bool read(uint8_t reg, int32_t* value, bool send_stop) override { + return read(reg, (uint32_t*)value, send_stop); + } + +private: + I2CMaster& master; + uint16_t address; + bool swap_bytes; + + bool write_register(uint8_t reg) { + master.write_async(address, ®, 1, false); + finish(); + return !master.has_error(); + } + + void finish() { + elapsedMillis timeout; + while (timeout <= timeout_millis) { + if (master.finished()) { + return; + } + } + } +}; + +#endif //I2C_DEVICE_H diff --git a/src/i2c_driver.h b/src/i2c_driver.h new file mode 100644 index 0000000..8242dbe --- /dev/null +++ b/src/i2c_driver.h @@ -0,0 +1,116 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) + +#ifndef I2C_DRIVER_H +#define I2C_DRIVER_H + +#include +#include +#include + +enum class I2CError { + // 'ok' means there were no errors since the last transaction started + ok = 0, + + // Remaining codes mean that something went wrong. + arbitration_lost = 1, // Another master interrupted + buffer_overflow = 2, // Not enough room in receive buffer to hold all the data. Bytes dropped. + buffer_underflow = 3, // Not enough data in transmit buffer to satisfy reader. Padding sent. + invalid_request = 4, // Caller asked Master to read more than 256 bytes in one go + master_pin_low_timeout = 5, // SCL or SDA held low for too long. Can be caused by a stuck slave. + master_not_ready = 6, // Caller failed to wait for one transaction to finish before starting the next + master_fifo_error = 7, // Master attempted to send or receive without a START. Programming error. + master_fifos_not_empty = 8, // Programming error. FIFOs not empty at start of transaction. + address_nak = 9, + data_nak = 10, + bit_error = 11 // Slave sent a 1 but found a 0 on the bus. Transaction aborted. +}; + +// Contains behaviour that's common to both masters and slaves. +class I2CDriver { +public: + // Indicates whether the driver is working or what happened + // in the last read/write + inline I2CError error() { + return _error; + } + + // True if the last operation failed. i.e. if last_error returns anything other than ok. + inline bool has_error() { + return _error > I2CError::ok; + } +protected: + volatile I2CError _error = I2CError::ok; +}; + +class I2CMaster : public I2CDriver { +public: + // Configures the master and enables it. You should call this before + // attempting to communicate with any slaves. + // 'frequency' determines the bus frequency in Hz. You must not set + // the speed higher than the highest speed of the slowest device on the bus. + // 100 kHz will work with any I2C device. Other common values are 400 kHz and 1 MHz. + // Returns true if the bus was acquired successfully. + virtual void begin(uint32_t frequency) = 0; + + // Makes the master release all resources that it gathered in begin() + // You only need to call this if you want to use the same pins for + // something else. + virtual void end() = 0; + + // False while the driver is doing work. e.g. reading or writing + // True when it's Ok to do another read or write + virtual bool finished() = 0; + + // Transmits the contents of buffer to the slave at the given address. + // The caller must not modify the buffer until the read is complete. + // Call finished() to see if the call has finished. + // Set 'send_stop' to true if this is the last transfer in the transaction. + // Set 'send_stop' to false if are going to make another transfer. + virtual void write_async(uint16_t address, uint8_t* buffer, size_t num_bytes, bool send_stop) = 0; + + // Reads the specified number of bytes and copies them into the supplied buffer. + // The caller must not modify the buffer until the read is complete. + // Set 'send_stop' to true if this is the last transfer in the transaction. + // Set 'send_stop' to false if are going to make another transfer. + // Call finished() to see if the call has finished. + virtual void read_async(uint16_t address, uint8_t* buffer, size_t num_bytes, bool send_stop) = 0; +}; + +class I2CSlave : public I2CDriver { +public: + // Start listening to the master on the given address. Makes the slave visible on the bus. + virtual void listen(uint16_t address) = 0; + + // Sets a callback to be called by the ISR each time + // the slave receives a block of data from the master. + virtual void after_receive(std::function callback) = 0; + + // Detach from the bus. The slave will no longer be visible to the master. + // Does nothing unless the slave is listening. + virtual void stop_listening() = 0; + + // Sets a callback to be called by the ISR just before + // the slave transmits a block of data to the master. + virtual void before_transmit(std::function callback) = 0; + + // Sets a callback to be called by the ISR each time + // each time the slave has sent a block of data to the master. + virtual void after_transmit(std::function callback) = 0; + + // Determines which data will be sent to the master the next time + // it reads from us. + // The master will receive up to 'numBytes' from us. If it demands + // more data it will be sent 0xFF until it ends the request. + // The call to onTransmit gives the actual number of bytes transmitted. + virtual void set_transmit_buffer(uint8_t* buffer, size_t size) = 0; + + // Determines where to put data we receive from the the master + // the next it writes to us. + // The master can send up to 'numBytes' of data to us. Extra bytes + // will be dropped. The call to onReceive gives the actual number + // of bytes received. + virtual void set_receive_buffer(uint8_t* buffer, size_t size) = 0; +}; + +#endif //I2C_DRIVER_H diff --git a/src/i2c_driver_wire.h b/src/i2c_driver_wire.h new file mode 100644 index 0000000..de7db50 --- /dev/null +++ b/src/i2c_driver_wire.h @@ -0,0 +1,184 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) + +#ifndef I2C_DRIVER_WIRE_H +#define I2C_DRIVER_WIRE_H + +#include +#include +#include "i2c_driver.h" +#ifdef __IMXRT1062__ +#include "imx_rt1060/imx_rt1060_i2c_driver.h" +#endif + +class I2CDriverWire : public Stream { +public: + // Size of RX and TX buffers. Feel free to change sizes if necessary. + static const size_t rx_buffer_length = 32; + static const size_t tx_buffer_length = 32; + + // Time to wait for a read or write to complete in millis + static const uint32_t timeout_millis = 200; + + // Indicates that there is no more data to read. + static const int no_more_bytes = -1; + + I2CDriverWire(I2CMaster& master, I2CSlave& slave) + : Stream(), master(master), slave(slave) { + } + + void begin() { + end(); + master.begin(master_frequency); + } + + void begin(int address) { + end(); + slave.set_receive_buffer(rxBuffer, rx_buffer_length); + slave.after_receive(std::bind(&I2CDriverWire::on_receive_wrapper, this, std::placeholders::_1)); + slave.before_transmit(std::bind(&I2CDriverWire::before_transmit, this)); + slave.listen((uint16_t)address); + } + + void end() { + master.end(); + slave.stop_listening(); + } + + void setClock(uint32_t frequency) { + master_frequency = frequency; + } + + void beginTransmission(int address) { + write_address = (uint16_t)address; + tx_next_byte_to_write = 0; + } + + uint8_t endTransmission(int stop = true) { + if (tx_next_byte_to_write > 0) { + master.write_async(write_address, tx_buffer, tx_next_byte_to_write, stop); + finish(); + } + return toWireResult(master.error()); + } + + size_t write(uint8_t data) override { + if (tx_next_byte_to_write < tx_buffer_length) { + tx_buffer[tx_next_byte_to_write++] = data; + return 1; + } + return 0; + } + + size_t write(const uint8_t* data, size_t length) override { + size_t avail = tx_buffer_length - tx_next_byte_to_write; + if (avail >= length) { + uint8_t* dest = tx_buffer + tx_next_byte_to_write; + memcpy(dest, data, length); + tx_next_byte_to_write += length; + return length; + } + return 0; + } + + uint8_t requestFrom(int address, int quantity, int stop = true) { + rx_bytes_available = quantity; + rx_next_byte_to_read = 0; + master.read_async(address, rxBuffer, min((size_t)quantity, rx_buffer_length), stop); + finish(); + return 0; + } + + inline int available() override { + return (int)(rx_bytes_available - rx_next_byte_to_read); + } + + int read() override { + if (rx_next_byte_to_read < rx_bytes_available) { + return rxBuffer[rx_next_byte_to_read++]; + } + return no_more_bytes; + } + + int peek() override { + if (rx_next_byte_to_read < rx_bytes_available) { + return rxBuffer[rx_next_byte_to_read]; + } + return no_more_bytes; + } + + inline void onReceive(void (* function)(int len)) { + on_receive = function; + } + + // A callback that's called by the I2C driver's interrupt + // service routine (ISR). + // WARNING: This method is called inside an ISR so it must be + // very, very fast. Avoid using it if at all possible. + // In particular, don't call write() in this method to prepare + // the transmit buffer. It's much better to fill the transmit + // buffer during loop(). + inline void onRequest(void (* function)()) { + on_request = function; + } + + using Print::write; + +private: + I2CMaster& master; + I2CSlave& slave; + uint32_t master_frequency = 100 * 1000U; + + void (* on_receive)(int len) = nullptr; + void (* on_request)() = nullptr; + + uint16_t write_address = 0; + uint8_t tx_buffer[tx_buffer_length] = {}; + size_t tx_next_byte_to_write = 0; + + uint8_t rxBuffer[rx_buffer_length] = {}; + size_t rx_bytes_available = 0; + size_t rx_next_byte_to_read = 0; + + static int toWireResult(I2CError status) { + if (status == I2CError::ok) return 0; + if (status == I2CError::buffer_overflow) return 1; + if (status == I2CError::address_nak) return 2; + if (status == I2CError::data_nak) return 3; + return 4; + } + + // Gives the application a chance to set up the transmit buffer + // during the ISR. + void before_transmit() { + tx_next_byte_to_write = 0; + if (on_request) { + on_request(); + } + slave.set_transmit_buffer(tx_buffer, tx_next_byte_to_write); + } + + void finish() { + elapsedMillis timeout; + while (timeout < timeout_millis) { + if (master.finished()) { + return; + } + } + Serial.println("Timed out waiting for transfer to finish."); + } + + inline void on_receive_wrapper(size_t num_bytes) { + rx_bytes_available = num_bytes; + rx_next_byte_to_read = 0; + if (on_receive) { + on_receive(num_bytes); + } + } +}; + +I2CDriverWire Wire(Master, Slave); +I2CDriverWire Wire1(Master1, Slave1); +I2CDriverWire Wire2(Master2, Slave2); + +#endif //I2C_DRIVER_WIRE_H diff --git a/src/i2c_register_slave.cpp b/src/i2c_register_slave.cpp new file mode 100644 index 0000000..3e3863e --- /dev/null +++ b/src/i2c_register_slave.cpp @@ -0,0 +1,58 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) + +#include +#include "i2c_register_slave.h" + +void I2CRegisterSlave::listen(uint16_t address) { + slave.listen(address); + slave.after_receive(std::bind(&I2CRegisterSlave::after_receive, this, std::placeholders::_1)); + slave.after_transmit(std::bind(&I2CRegisterSlave::after_transmit, this)); + wait_for_reg_num(); +}; + +void I2CRegisterSlave::after_receive(int len) { + got_reg_num = !got_reg_num; + if (got_reg_num) { + uint8_t reg_num = rx_buffer[0]; + if (len > 1) { + // Master sent register number and data to write to a buffer in one go + if (reg_num < mutable_buffer_size) { + uint8_t* buffer = mutable_buffer + reg_num; + uint8_t buffer_size = mutable_buffer_size - reg_num; + size_t copy_len = len - 1; + if (copy_len > buffer_size) { + copy_len = buffer_size; + } + memcpy(buffer, rx_buffer + 1, copy_len); + } + // else it's not a valid registry. Ignore it. + got_reg_num = false; + } else { + if (reg_num < mutable_buffer_size) { + // The coming read or write is aimed at the mutable buffer. + uint8_t* buffer = mutable_buffer + reg_num; + uint8_t buffer_size = mutable_buffer_size - reg_num; + slave.set_receive_buffer(buffer, buffer_size); + slave.set_transmit_buffer(buffer, buffer_size); + } else { + // reg_num is too big for a write. Discard the next write if there is one. + slave.set_receive_buffer(rx_buffer, 0); + + uint8_t offset = reg_num - mutable_buffer_size; + if (offset < read_only_buffer_size) { + slave.set_transmit_buffer(read_only_buffer + offset, read_only_buffer_size - offset); + } else { + // reg_num is too big for a write. The next read will get dummy data. + slave.set_transmit_buffer(read_only_buffer, 0); + } + } + } + } + if (!got_reg_num) { + if (after_write_callback) { + after_write_callback(rx_buffer[0]); + } + wait_for_reg_num(); + } +} diff --git a/src/i2c_register_slave.h b/src/i2c_register_slave.h new file mode 100644 index 0000000..a93f88f --- /dev/null +++ b/src/i2c_register_slave.h @@ -0,0 +1,103 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) + +#ifndef I2C_REGISTER_SLAVE_H +#define I2C_REGISTER_SLAVE_H + +#include +#include +#include "i2c_driver.h" + +#ifdef __IMXRT1062__ +#include "imx_rt1060/imx_rt1060_i2c_driver.h" +#endif + +// Needs to be as big as the largest register plus 1 byte for the registry number. +#define REG_SLAVE_WRITE_BUFFER_LENGTH 9 + +class II2CRegisterSlave { +public: + // Calls listen() on the underlying slave driver and then attaches out even + // handlers. Don't call listen on the underlying slave directly or it won't work. + virtual void listen(uint16_t address) = 0; + + // Add a callback to be notified when the master has read a register. + // This is often used to clear the "new data available" flag if + // there is one. + virtual void after_read(std::function callback) = 0; + + // Add a callback to be notified when the master has written to a + // register. + virtual void after_write(std::function callback) = 0; +}; + +// Wraps I2CSlave to make it easy to implement an I2C slave who's interface +// is defined as a set of "registers" that the master can read or write. +// +// The application should update the slave's read_only buffers in the main +// loop. The master is expected to read the registers asynchronously without +// the application needing to create an interrupt service routine to respond +// to the master. +class I2CRegisterSlave : public II2CRegisterSlave { +public: + // 'mutable_buffer' a set of registers that the master can write to + // as well as read. Normally used to configure the slave. Use a zero + // length buffer if it's not required. Application code should avoid + // modifying these buffers to avoid the risk of missing a change by + // the master. + // 'read_only_buffer' a set of registers that can be read by the master + // but not written to. These are usually updated in the main application + // loop. The master is free to read them whenever it pleases. + I2CRegisterSlave(I2CSlave& slave, + uint8_t* mutable_buffer, size_t num_mutable_bytes, + uint8_t* read_only_buffer, size_t num_read_only_bytes) + : slave(slave), + mutable_buffer(mutable_buffer), mutable_buffer_size(num_mutable_bytes), + read_only_buffer(read_only_buffer), read_only_buffer_size(num_read_only_bytes) { + } + + // Calls listen() on the underlying slave driver and then attaches out even + // handlers. Don't call listen on the underlying slave directly or it won't work. + void listen(uint16_t address) override; + + // Add a callback to be notified when the master has read a register. + // This is often used to clear the "new data available" flag if + // there is one. + inline void after_read(std::function callback) override { + after_read_callback = std::move(callback); + } + + // Add a callback to be notified when the master has written to a + // register. + inline void after_write(std::function callback) override { + after_write_callback = std::move(callback); + } + +private: + I2CSlave& slave; + uint8_t rx_buffer[REG_SLAVE_WRITE_BUFFER_LENGTH] = {}; + bool got_reg_num = false; + uint8_t* const mutable_buffer; + const size_t mutable_buffer_size; + uint8_t* const read_only_buffer; + const size_t read_only_buffer_size; + std::function after_read_callback = nullptr; + std::function after_write_callback = nullptr; + + void after_receive(int len); + + inline void after_transmit() { + wait_for_reg_num(); + if (after_read_callback) { + after_read_callback(rx_buffer[0]); + } + } + + inline void wait_for_reg_num() { + got_reg_num = false; + slave.set_receive_buffer(rx_buffer, REG_SLAVE_WRITE_BUFFER_LENGTH); + slave.set_transmit_buffer(mutable_buffer, 0); + } +}; + +#endif //I2C_REGISTER_SLAVE_H diff --git a/src/imx_rt1060/imx_rt1060.h b/src/imx_rt1060/imx_rt1060.h new file mode 100644 index 0000000..44c7f05 --- /dev/null +++ b/src/imx_rt1060/imx_rt1060.h @@ -0,0 +1,57 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) + +#ifndef IMX_RT1060_H +#define IMX_RT1060_H + +#include + +typedef struct { + const uint32_t VERID; + const uint32_t PARAM; + const uint32_t unused1; + const uint32_t unused2; + volatile uint32_t MCR; // 010 + volatile uint32_t MSR; // 014 + volatile uint32_t MIER; // 018 + volatile uint32_t MDER; // 01C + volatile uint32_t MCFGR0; // 020 + volatile uint32_t MCFGR1; // 024 + volatile uint32_t MCFGR2; // 028 + volatile uint32_t MCFGR3; // 02C + volatile uint32_t unused3[4]; + volatile uint32_t MDMR; // 040 + volatile uint32_t unused4; + volatile uint32_t MCCR0; // 048 + volatile uint32_t unused5; + volatile uint32_t MCCR1; // 050 + volatile uint32_t unused6; + volatile uint32_t MFCR; // 058 + volatile uint32_t MFSR; // 05C + volatile uint32_t MTDR; // 060 + volatile uint32_t unused7[3]; + volatile uint32_t MRDR; // 070 + volatile uint32_t unused8[39]; + volatile uint32_t SCR; // 110 + volatile uint32_t SSR; // 114 + volatile uint32_t SIER; // 118 + volatile uint32_t SDER; // 11C + volatile uint32_t unused9; + volatile uint32_t SCFGR1; // 124 + volatile uint32_t SCFGR2; // 128 + volatile uint32_t unused10[5]; + volatile uint32_t SAMR; // 140 + volatile uint32_t unused11[3]; + volatile uint32_t SASR; // 150 + volatile uint32_t STAR; // 154 + volatile uint32_t unused13[2]; + volatile uint32_t STDR; // 160 + volatile uint32_t unused14[3]; + volatile uint32_t SRDR; // 170 +} IMXRT_LPI2C_Registers; +#define LPI2C1 (*(IMXRT_LPI2C_Registers *)0x403F0000) +#define LPI2C2 (*(IMXRT_LPI2C_Registers *)0x403F4000) // Not connected to any pins on the Teensy 4.0 +#define LPI2C3 (*(IMXRT_LPI2C_Registers *)0x403F8000) +#define LPI2C4 (*(IMXRT_LPI2C_Registers *)0x403FC000) + +#endif //IMX_RT1060_H \ No newline at end of file diff --git a/src/imx_rt1060/imx_rt1060_i2c_driver.cpp b/src/imx_rt1060/imx_rt1060_i2c_driver.cpp new file mode 100644 index 0000000..75bc01c --- /dev/null +++ b/src/imx_rt1060/imx_rt1060_i2c_driver.cpp @@ -0,0 +1,668 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) +// +// Fragments of this code copied from WireIMXRT.cpp © Paul Stoffregen. +// Please support the Teensy project at pjrc.com. + +//#define DEBUG_I2C // Uncomment to enable debug tools +#ifdef DEBUG_I2C +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#include +#endif + +#include +#include +#include "imx_rt1060_i2c_driver.h" + +#define DUMMY_BYTE 0x00 // Used when there's no real data to write. +#define NUM_FIFOS 4 // Number of Rx and Tx FIFOs available to master +#define MASTER_READ 1 // Makes the address a read request +#define MASTER_WRITE 0 // Makes the address a write request +#define MAX_MASTER_READ_LENGTH 256 // Maximum number of bytes that can be read by a single Master read + +// Debug tools +#ifdef DEBUG_I2C +static void log_slave_status_register(uint32_t ssr); +static void log_master_status_register(uint32_t msr); +static void log_master_control_register(const char* message, uint32_t mcr); +#endif + +static uint8_t empty_buffer[0]; + +I2CBuffer::I2CBuffer() : buffer(empty_buffer) { +} + +static void initialise_pin(IMX_RT1060_I2CBase::PinInfo pin) { + *(portControlRegister(pin.pin)) |= IOMUXC_PAD_PKE | IOMUXC_PAD_PUE | IOMUXC_PAD_PUS(3); + *(portConfigRegister(pin.pin)) = pin.mux_val; + if (pin.select_input_register) { + *(pin.select_input_register) = pin.select_val; + } +} + +static void initialise_common(IMX_RT1060_I2CBase::Config hardware) { + // Set LPI2C Clock to 24MHz. Required by slaves as well as masters. + CCM_CSCDR2 = (CCM_CSCDR2 & ~CCM_CSCDR2_LPI2C_CLK_PODF(63)) | CCM_CSCDR2_LPI2C_CLK_SEL; + hardware.clock_gate_register |= hardware.clock_gate_mask; + + // Setup SDA and SCL pins and registers + initialise_pin(hardware.sda_pin); + initialise_pin(hardware.scl_pin); +} + +static void stop(IMXRT_LPI2C_Registers* port, IRQ_NUMBER_t irq) { + // Halt and reset Master Mode if it's running + port->MCR = (LPI2C_MCR_RST | LPI2C_MCR_RRF | LPI2C_MCR_RTF); + port->MCR = 0; + + // Halt and reset Slave Mode if it's running + port->SCR = (LPI2C_SCR_RST | LPI2C_SCR_RRF | LPI2C_SCR_RTF); + port->SCR = 0; + + // Disable interrupts + NVIC_DISABLE_IRQ(irq); + attachInterruptVector(irq, nullptr); +} + +IMX_RT1060_I2CMaster::IMX_RT1060_I2CMaster(IMXRT_LPI2C_Registers* port, IMX_RT1060_I2CBase::Config& config, + void (* isr)()) + : port(port), config(config), isr(isr) { +} + +void IMX_RT1060_I2CMaster::begin(uint32_t frequency) { + // Make sure master mode is disabled before configuring it. + stop(port, config.irq); + + // Setup pins and master clock + initialise_common(config); + + // Configure and Enable Master Mode + // Set FIFO watermarks. Determines when the RDF and TDF interrupts happen + port->MFCR = LPI2C_MFCR_RXWATER(0) | LPI2C_MFCR_TXWATER(0); + set_clock(frequency); + // Setup interrupt service routine. + attachInterruptVector(config.irq, isr); + port->MIER = LPI2C_MIER_RDIE | LPI2C_MIER_SDIE | LPI2C_MIER_NDIE | LPI2C_MIER_ALIE | LPI2C_MIER_FEIE | LPI2C_MIER_PLTIE; + NVIC_ENABLE_IRQ(config.irq); + // Enable master + port->MCR = LPI2C_MCR_MEN; +} + +void IMX_RT1060_I2CMaster::end() { + stop(port, config.irq); +} + +inline bool IMX_RT1060_I2CMaster::finished() { + return state >= State::idle; +} + +void IMX_RT1060_I2CMaster::write_async(uint16_t address, uint8_t* buffer, size_t num_bytes, bool send_stop) { + if (!start(address, MASTER_WRITE)) { + return; + } + + buff.initialise(buffer, num_bytes); + stop_on_completion = send_stop; + port->MIER |= LPI2C_MIER_TDIE; +} + +void IMX_RT1060_I2CMaster::read_async(uint16_t address, uint8_t* buffer, size_t num_bytes, bool send_stop) { + if (num_bytes > MAX_MASTER_READ_LENGTH) { + _error = I2CError::invalid_request; + return; + } + + if (!start(address, MASTER_READ)) { + return; + } + + buff.initialise(buffer, num_bytes); + port->MTDR = LPI2C_MTDR_CMD_RECEIVE | (num_bytes - 1); + + if (send_stop) { + port->MTDR = LPI2C_MTDR_CMD_STOP; + } +} + +// Do not call this method directly +void IMX_RT1060_I2CMaster::_interrupt_service_routine() { + uint32_t msr = port->MSR; +// Serial.print("ISR: enter: "); +// log_master_status_register(msr); + + if (msr & (LPI2C_MSR_NDF | LPI2C_MSR_ALF | LPI2C_MSR_FEF | LPI2C_MSR_PLTF)) { + if (msr & LPI2C_MSR_NDF) { + port->MSR = LPI2C_MSR_NDF; + if (state == State::starting) { + _error = I2CError::address_nak; + } else { + _error = I2CError::data_nak; + } + } + if (msr & LPI2C_MSR_ALF) { + port->MSR = LPI2C_MSR_ALF; + _error = I2CError::arbitration_lost; + } + if (msr & LPI2C_MSR_FEF) { + port->MSR = LPI2C_MSR_FEF; + if (!has_error()) { + _error = I2CError::master_fifo_error; + } + // else FEF was triggered by another error. Ignore it. + } + if (msr & LPI2C_MSR_PLTF) { + port->MSR = LPI2C_MSR_PLTF; + _error = I2CError::master_pin_low_timeout; + } + if (state != State::stopping) { + state = State::stopping; + abort_transaction_async(); + } + // else already trying to end the transaction + } + + if (msr & LPI2C_MSR_SDF) { + port->MIER &= ~LPI2C_MIER_TDIE; // We don't want to handle TDF if we can avoid it. + state = State::stopped; + port->MSR = LPI2C_MSR_SDF; + } + + if (msr & LPI2C_MSR_RDF) { + if (is_read) { + if (buff.not_stated_reading()) { + _error = I2CError::ok; + state = State::transferring; + } + if (state == State::transferring) { + buff.write(port->MRDR); + } else { + port->MCR |= LPI2C_MCR_RRF; + } + if (buff.finished_reading()) { + if (tx_fifo_count() == 1) { + state = State::stopping; + } else { + state = State::transfer_complete; + } + port->MCR &= ~LPI2C_MCR_MEN; // Avoids triggering PLTF if we didn't send a STOP + } + } else { + // This is a write transaction. We shouldn't have got a read. + state = State::stopping; + abort_transaction_async(); + } + } + + if (!is_read && (msr & LPI2C_MSR_TDF)) { + if (buff.not_stated_writing()) { + _error = I2CError::ok; + state = State::transferring; + } + if (state == State::transferring) { + // Fill the transmit buffer + uint32_t fifo_space = NUM_FIFOS - tx_fifo_count(); + while (buff.has_data_available() && fifo_space > 0) { + port->MTDR = LPI2C_MTDR_CMD_TRANSMIT | buff.read(); + fifo_space--; + } + if (buff.finished_writing() && tx_fifo_count() == 0) { + port->MIER &= ~LPI2C_MIER_TDIE; + if (stop_on_completion) { + state = State::stopping; + port->MTDR = LPI2C_MTDR_CMD_STOP; + } else { + state = State::transfer_complete; + } + port->MCR &= ~LPI2C_MCR_MEN; // Avoids triggering PLTF if we didn't send a STOP + } + } + // else ignore it. This flag is frequently set in read transfers. + } +} + +inline uint8_t IMX_RT1060_I2CMaster::tx_fifo_count() { + return port->MFSR & 0x7; +} + +inline uint8_t IMX_RT1060_I2CMaster::rx_fifo_count() { + return (port->MFSR >> 16) & 0x07; +} + +inline void IMX_RT1060_I2CMaster::clear_all_msr_flags() { + port->MSR &= (LPI2C_MSR_DMF | LPI2C_MSR_PLTF | LPI2C_MSR_FEF | + LPI2C_MSR_ALF | LPI2C_MSR_NDF | LPI2C_MSR_SDF | + LPI2C_MSR_EPF | LPI2C_MSR_RDF | LPI2C_MSR_TDF); +} + +bool IMX_RT1060_I2CMaster::start(uint16_t address, uint32_t direction) { + if (!finished()) { + // We haven't completed the previous transaction yet + #ifdef DEBUG_I2C + Serial.print("Master: Cannot start. Transaction still in progress. Status: "); + Serial.println((int)_error); + #endif + _error = I2CError::master_not_ready; + return false; + } + + // Start a new transaction + is_read = direction; + state = State::starting; + + // Make sure the FIFOs are empty before we start. + if (tx_fifo_count() > 0 || rx_fifo_count() > 0) { + // This should never happen. + #ifdef DEBUG_I2C + Serial.println("Master: FIFOs not empty in start()."); + #endif + _error = I2CError::master_fifos_not_empty; + abort_transaction_async(); + return false; + } + + // Clear status flags + clear_all_msr_flags(); + + // Send a START to the slave at 'address' + port->MCR |= LPI2C_MCR_MEN; + uint8_t i2c_address = (address & 0x7F) << 1; + port->MTDR = LPI2C_MTDR_CMD_START | i2c_address | direction; + + return true; +} + +// In theory, you can use MCR[RST] to reset the master but +// this doesn't seem to work in some circumstances. e.g. +// When the master is trying to receive more bytes. +void IMX_RT1060_I2CMaster::abort_transaction_async() { + #ifdef DEBUG_I2C + Serial.println("Master: abort_transaction"); + log_master_status_register(port->MSR); + #endif + + // Clear out any commands that haven't been sent + port->MCR |= LPI2C_MCR_RTF; + port->MCR |= LPI2C_MCR_RRF; + + // Send a stop if we haven't already done so + if (!(port->MSR & (LPI2C_MSR_SDF))) { + port->MTDR = LPI2C_MTDR_CMD_STOP; + } +} + +// Supports 100 kHz, 400 kHz and 1 MHz modes. +void IMX_RT1060_I2CMaster::set_clock(uint32_t frequency) { + if (frequency < 400000) { + // Use Standard Mode - up to 100 kHz + port->MCCR0 = LPI2C_MCCR0_CLKHI(55) | LPI2C_MCCR0_CLKLO(59) | + LPI2C_MCCR0_DATAVD(25) | LPI2C_MCCR0_SETHOLD(40); + port->MCFGR1 = LPI2C_MCFGR1_PRESCALE(1); + port->MCFGR2 = LPI2C_MCFGR2_FILTSDA(5) | LPI2C_MCFGR2_FILTSCL(5) | + LPI2C_MCFGR2_BUSIDLE(3900); + } else if (frequency < 1000000) { + // Use Fast Mode - up to 400 kHz + port->MCCR0 = LPI2C_MCCR0_CLKHI(26) | LPI2C_MCCR0_CLKLO(28) | + LPI2C_MCCR0_DATAVD(12) | LPI2C_MCCR0_SETHOLD(18); + port->MCFGR1 = LPI2C_MCFGR1_PRESCALE(0); + port->MCFGR2 = LPI2C_MCFGR2_FILTSDA(2) | LPI2C_MCFGR2_FILTSCL(2) | + LPI2C_MCFGR2_BUSIDLE(3900); + } else { + // Use Fast Mode Plus - up to 1 MHz + port->MCCR0 = LPI2C_MCCR0_CLKHI(9) | LPI2C_MCCR0_CLKLO(10) | + LPI2C_MCCR0_DATAVD(4) | LPI2C_MCCR0_SETHOLD(7); + port->MCFGR1 = LPI2C_MCFGR1_PRESCALE(0); + port->MCFGR2 = LPI2C_MCFGR2_FILTSDA(1) | LPI2C_MCFGR2_FILTSCL(1) | + LPI2C_MCFGR2_BUSIDLE(3900); + } + port->MCCR1 = port->MCCR0; + port->MCFGR3 = LPI2C_MCFGR3_PINLOW(3900); // Pin low timeout +} + +void IMX_RT1060_I2CSlave::listen(uint16_t address) { + // Make sure slave mode is disabled before configuring it. + stop_listening(); + + initialise_common(config); + + // Set the Slave Address + port->SAMR = address << 1U; + // Enable clock stretching + port->SCFGR1 |= (LPI2C_SCFGR1_TXDSTALL | LPI2C_SCFGR1_RXSTALL); + // Set up interrupts + attachInterruptVector(config.irq, isr); + port->SIER |= (LPI2C_SIER_RSIE | LPI2C_SIER_SDIE | LPI2C_SIER_TDIE | LPI2C_SIER_RDIE); + NVIC_ENABLE_IRQ(config.irq); + + // Enable Slave Mode + port->SCR = LPI2C_SCR_SEN; +} + +inline void IMX_RT1060_I2CSlave::stop_listening() { + // End slave mode + stop(port, config.irq); +} + +inline void IMX_RT1060_I2CSlave::after_receive(std::function callback) { + after_receive_callback = callback; +} + +inline void IMX_RT1060_I2CSlave::before_transmit(std::function callback) { + before_transmit_callback = callback; +} + +inline void IMX_RT1060_I2CSlave::after_transmit(std::function callback) { + after_transmit_callback = callback; +} + +inline void IMX_RT1060_I2CSlave::set_transmit_buffer(uint8_t* buffer, size_t size) { + tx_buffer.initialise(buffer, size); +} + +inline void IMX_RT1060_I2CSlave::set_receive_buffer(uint8_t* buffer, size_t size) { + rx_buffer.initialise(buffer, size); +} + +// WARNING: Do not call directly. +void IMX_RT1060_I2CSlave::_interrupt_service_routine() { + // Read the slave status register + uint32_t ssr = port->SSR; +// log_slave_status_register(ssr); + + if (ssr & LPI2C_SSR_AM0F) { + port->SASR; // Read SASR to clear to the address flag. (Just for neatness) + } + + if (ssr & (LPI2C_SSR_RSF | LPI2C_SSR_SDF)) { + // Detected Repeated START or STOP + port->SSR = (LPI2C_SSR_RSF | LPI2C_SSR_SDF); + end_of_frame(); + } + + if (ssr & LPI2C_SSR_RDF) { + // Received Data + uint32_t srdr = port->SRDR; // Read the Slave Received Data Register + if (srdr & LPI2C_SRDR_SOF) { + // Start of Frame (The first byte since a (repeated) START or STOP condition) + _error = I2CError::ok; + if (rx_buffer.initialised()) { + rx_buffer.reset(); + state = State::receiving; + } + } + uint8_t data = srdr & LPI2C_SRDR_DATA(0xFF); + if (rx_buffer.initialised()) { + if (!rx_buffer.write(data)) { + // The buffer is already full. + _error = I2CError::buffer_overflow; + // TODO: The spec says we should send NACK but how? + } + } else { + // We are not interested in reading anything. + // TODO: The spec says we should send NACK but how? + // Clear previous status and error + state = State::idle; + } + } + + if (ssr & LPI2C_SSR_TDF) { + // Transmit Data Request - Master is requesting a byte + bool start_of_frame = state >= State::idle; + if (start_of_frame) { + _error = I2CError::ok; + state = State::transmitting; + if (before_transmit_callback) { + before_transmit_callback(); + } + } + if (tx_buffer.initialised()) { + if (start_of_frame) { + tx_buffer.reset(); + } + if (tx_buffer.has_data_available()) { + port->STDR = tx_buffer.read(); + } else { + port->STDR = DUMMY_BYTE; + // WARNING: We're always asked for one more byte then + // the master actually requested. Use trailing_byte_sent + // to work out whether the master actually asked for too much data. + if (!trailing_byte_sent) { + trailing_byte_sent = true; + } else { + _error = I2CError::buffer_underflow; + } + } + } else { + // We don't have any data to send. + // TODO: The spec says we should send NACK but how? + // Just send a dummy value for now. + port->STDR = DUMMY_BYTE; + _error = I2CError::buffer_underflow; // Clear previous status + } + } + + if (ssr & LPI2C_SSR_FEF) { + port->SSR = LPI2C_SSR_FEF; + // Will not happen if clock stretching is enabled. + } + + if (ssr & LPI2C_SSR_BEF) { + #ifdef DEBUG_I2C + Serial.println("I2C Slave: Bit Error"); + #endif + // The bus is probably stuck at this point. + // I don't think the slave can clear the fault. The master has to do it. + port->SSR = LPI2C_SSR_BEF; + state = State::aborted; + _error = I2CError::bit_error; + end_of_frame(); + } +} + +// Called from within the ISR when we receive a Repeated START or STOP +void IMX_RT1060_I2CSlave::end_of_frame() { + if (state == State::receiving) { + if (after_receive_callback) { + after_receive_callback(rx_buffer.get_bytes_written()); + } + } else if (state == State::transmitting) { + trailing_byte_sent = false; + if (after_transmit_callback) { + after_transmit_callback(); + } + } + #ifdef DEBUG_I2C + else if (state != State::idle) { + Serial.print("Unexpected 'End of Frame'. State: "); + Serial.println((int)state); + } + if (_error == I2CError::bit_error) { + Serial.println("Transaction aborted because of bit error."); + } + #endif + state = State::idle; +} + +IMX_RT1060_I2CBase::Config i2c1_config = { + CCM_CCGR2, + CCM_CCGR2_LPI2C1(CCM_CCGR_ON), + IMX_RT1060_I2CBase::PinInfo{18U, 3U | 0x10U, &IOMUXC_LPI2C1_SDA_SELECT_INPUT, 1U}, + IMX_RT1060_I2CBase::PinInfo{19U, 3U | 0x10U, &IOMUXC_LPI2C1_SCL_SELECT_INPUT, 1U}, + false, + {}, + {}, + IRQ_LPI2C1 +}; + +IMX_RT1060_I2CBase::Config i2c3_config = { + CCM_CCGR2, + CCM_CCGR2_LPI2C3(CCM_CCGR_ON), + IMX_RT1060_I2CBase::PinInfo{17U, 1U | 0x10U, &IOMUXC_LPI2C3_SDA_SELECT_INPUT, 2U}, + IMX_RT1060_I2CBase::PinInfo{16U, 1U | 0x10U, &IOMUXC_LPI2C3_SCL_SELECT_INPUT, 2U}, + true, + IMX_RT1060_I2CBase::PinInfo{36U, 2U | 0x10U, &IOMUXC_LPI2C3_SDA_SELECT_INPUT, 1U}, + IMX_RT1060_I2CBase::PinInfo{37U, 2U | 0x10U, &IOMUXC_LPI2C3_SCL_SELECT_INPUT, 1U}, + IRQ_LPI2C3 +}; + +IMX_RT1060_I2CBase::Config i2c4_config = { + CCM_CCGR6, + CCM_CCGR6_LPI2C4_SERIAL(CCM_CCGR_ON), + IMX_RT1060_I2CBase::PinInfo{25U, 0U | 0x10U, &IOMUXC_LPI2C4_SDA_SELECT_INPUT, 1U}, + IMX_RT1060_I2CBase::PinInfo{24U, 0U | 0x10U, &IOMUXC_LPI2C4_SCL_SELECT_INPUT, 1U}, + false, + {}, + {}, + IRQ_LPI2C4 +}; + +static void master_isr(); + +IMX_RT1060_I2CMaster Master = IMX_RT1060_I2CMaster(&LPI2C1, i2c1_config, master_isr); + +static void master_isr() { + Master._interrupt_service_routine(); +} + +static void master1_isr(); + +IMX_RT1060_I2CMaster Master1 = IMX_RT1060_I2CMaster(&LPI2C3, i2c3_config, master1_isr); + +static void master1_isr() { + Master1._interrupt_service_routine(); +} + +static void master2_isr(); + +IMX_RT1060_I2CMaster Master2 = IMX_RT1060_I2CMaster(&LPI2C4, i2c4_config, master2_isr); + +static void master2_isr() { + Master2._interrupt_service_routine(); +} + +static void slave_isr(); + +IMX_RT1060_I2CSlave Slave = IMX_RT1060_I2CSlave(&LPI2C1, i2c1_config, slave_isr); + +static void slave_isr() { + Slave._interrupt_service_routine(); +} + +static void slave1_isr(); + +IMX_RT1060_I2CSlave Slave1 = IMX_RT1060_I2CSlave(&LPI2C3, i2c3_config, slave1_isr); + +static void slave1_isr() { + Slave1._interrupt_service_routine(); +} + +static void slave2_isr(); + +IMX_RT1060_I2CSlave Slave2 = IMX_RT1060_I2CSlave(&LPI2C4, i2c4_config, slave2_isr); + +static void slave2_isr() { + Slave2._interrupt_service_routine(); +} + +#ifdef DEBUG_I2C +static void log_master_control_register(const char* message, uint32_t mcr) { + Serial.print(message); + Serial.print(" MCR: "); + Serial.print(mcr); + Serial.println(""); +} + +static void log_master_status_register(uint32_t msr) { + if (msr) { + Serial.print("MSR Flags: "); + } + if (msr & LPI2C_MSR_BBF) { + Serial.print("BBF "); + } + if (msr & LPI2C_MSR_MBF) { + Serial.print("MBF "); + } + if (msr & LPI2C_MSR_DMF) { + Serial.print("DMF "); + } + if (msr & LPI2C_MSR_PLTF) { + Serial.print("PLTF "); + } + if (msr & LPI2C_MSR_FEF) { + Serial.print("FEF "); + } + if (msr & LPI2C_MSR_ALF) { + Serial.print("ALF "); + } + if (msr & LPI2C_MSR_NDF) { + Serial.print("NDF "); + } + if (msr & LPI2C_MSR_SDF) { + Serial.print("SDF "); + } + if (msr & LPI2C_MSR_EPF) { + Serial.print("EPF "); + } + if (msr & LPI2C_MSR_RDF) { + Serial.print("RDF "); + } + if (msr & LPI2C_MSR_TDF) { + Serial.print("TDF "); + } + if (msr) { + Serial.println(); + } +} + +static void log_slave_status_register(uint32_t ssr) { + if (ssr) { + Serial.print("SSR Flags: "); + } + if (ssr & LPI2C_SSR_BBF) { + Serial.print("BBF "); + } + if (ssr & LPI2C_SSR_SBF) { + Serial.print("SBF "); + } + if (ssr & LPI2C_SSR_SARF) { + Serial.print("SARF "); + } + if (ssr & LPI2C_SSR_GCF) { + Serial.print("GCF "); + } + if (ssr & LPI2C_SSR_AM1F) { + Serial.print("GCF "); + } + if (ssr & LPI2C_SSR_AM0F) { + Serial.print("AM0F "); + } + if (ssr & LPI2C_SSR_FEF) { + Serial.print("FEF "); + } + if (ssr & LPI2C_SSR_BEF) { + Serial.print("BBF "); + } + if (ssr & LPI2C_SSR_SDF) { + Serial.print("SDF "); + } + if (ssr & LPI2C_SSR_RSF) { + Serial.print("RSF "); + } + if (ssr & LPI2C_SSR_TAF) { + Serial.print("TAF "); + } + if (ssr & LPI2C_SSR_AVF) { + Serial.print("AVF "); + } + if (ssr & LPI2C_SSR_RDF) { + Serial.print("RDF "); + } + if (ssr & LPI2C_SSR_TDF) { + Serial.print("TDF "); + } + if (ssr) { + Serial.println(); + } +} +#pragma clang diagnostic pop +#endif //DEBUG_I2C diff --git a/src/imx_rt1060/imx_rt1060_i2c_driver.h b/src/imx_rt1060/imx_rt1060_i2c_driver.h new file mode 100644 index 0000000..2c93792 --- /dev/null +++ b/src/imx_rt1060/imx_rt1060_i2c_driver.h @@ -0,0 +1,217 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) + +#ifndef IMX_RT1060_I2C_DRIVER_H +#define IMX_RT1060_I2C_DRIVER_H + +#include +#include +#include "imx_rt1060.h" +#include "../i2c_driver.h" + +// A read or write buffer. +// You cannot use the same buffer for both reading and writing. +// This class is an implementation detail of the driver and +// should not be used elsewhere. +class I2CBuffer { +public: + I2CBuffer(); + + // May be called inside or outside the ISR. + // This should be safe because it can never be called while + // the ISR is trying to read from the buffer or write to it. + inline void initialise(uint8_t* new_buffer, size_t new_size) { + reset(); + buffer = new_buffer; + size = new_size; + } + + inline bool initialised() { + return size > 0; + } + + // Empties the buffer. + inline void reset() { + next_index = 0; + } + + // Returns 'false' if the buffer was already full. + inline bool write(uint8_t data) { + if (next_index == size) { + return false; + } else { + buffer[next_index++] = data; + return true; + } + } + + inline size_t get_bytes_written() { + return next_index; + } + + // Caller is responsible for preventing a read beyond the end of the buffer. + inline uint8_t read() { + return buffer[next_index++]; + } + + inline bool not_stated_writing() { + return next_index == 0; + } + + inline bool finished_writing() { + return next_index == size; + } + + inline bool not_stated_reading() { + return next_index == 0; + } + + inline bool finished_reading() { + return next_index == size; + } + + inline bool has_data_available() { + return next_index < size; + } + +private: + volatile uint8_t* buffer; + volatile size_t size = 0; + volatile size_t next_index = 0; +}; + +class IMX_RT1060_I2CBase { +public: + typedef struct { + const uint8_t pin; // The Teensy 4.0 pin number + const uint32_t mux_val; // Value to set for mux; + volatile uint32_t* select_input_register; // Which register controls the selection + const uint32_t select_val; // Value for that selection + } PinInfo; + + typedef struct { + volatile uint32_t& clock_gate_register; + uint32_t clock_gate_mask; + PinInfo sda_pin; // The default SDA pin + PinInfo scl_pin; // The default SCL pin + bool has_alternatives; // True if there are alternative pins + PinInfo alternative_sda_pin; // The alternative SDA pin. Undefined if has_alternatives is false + PinInfo alternative_scl_pin; // The alternative SCL pin. Undefined if has_alternatives is false + IRQ_NUMBER_t irq; // The interrupt request number for this port + } Config; +}; + +class IMX_RT1060_I2CMaster : public I2CMaster { +public: + IMX_RT1060_I2CMaster(IMXRT_LPI2C_Registers* port, IMX_RT1060_I2CBase::Config& config, void (* isr)()); + + // Supports the following frequencies: + // 100,000 - Standard Mode - up to 100 kHz + // 400,000 - Fast Mode - up to 400 kHz + // 1000,000 - Fast Mode Plus - up to 1 MHz + // If you set frequency to any other value the system will use + // the fastest mode which is slower than the given value. + // e.g. Setting frequency to 200,000 will give you a 100 kHz bus. + void begin(uint32_t frequency) override; + + void end() override; + + bool finished() override; + + void write_async(uint16_t address, uint8_t* buffer, size_t num_bytes, bool send_stop) override; + + void read_async(uint16_t address, uint8_t* buffer, size_t num_bytes, bool send_stop) override; + + // DO NOT call this method directly. + void _interrupt_service_routine(); +private: + enum class State { + // Busy states + starting = 0, // Waiting for START to be sent and acknowledged + transferring, // In a transfer + stopping, // Transfer complete or aborted. Waiting for STOP. + + // 'idle' and above mean that the driver has finished + // whatever it was doing and is ready to do more work. + idle = 100, // Not in a transaction + transfer_complete, // Transfer has finished and caller has not requested a STOP. + stopped // Transaction has finished. STOP sent. + }; + + IMXRT_LPI2C_Registers* const port; + IMX_RT1060_I2CBase::Config& config; + I2CBuffer buff = I2CBuffer(); + volatile State state = State::idle; + volatile uint32_t is_read = false; // True for a receivve transfer + volatile bool stop_on_completion = false; // True if the transmit transfer requires a stop. + + void (* isr)(); + void set_clock(uint32_t frequency); + void abort_transaction_async(); + bool start(uint16_t address, uint32_t direction); + uint8_t tx_fifo_count(); + uint8_t rx_fifo_count(); + void clear_all_msr_flags(); +}; + +extern IMX_RT1060_I2CMaster Master; +extern IMX_RT1060_I2CMaster Master1; +extern IMX_RT1060_I2CMaster Master2; + +class IMX_RT1060_I2CSlave : public I2CSlave +{ +public: + IMX_RT1060_I2CSlave(IMXRT_LPI2C_Registers* port, IMX_RT1060_I2CBase::Config& config, void (* isr)()) + : port(port), config(config), isr(isr) { + } + + void listen(uint16_t address) override; + + void stop_listening() override; + + void after_receive(std::function callback) override; + + void before_transmit(std::function callback) override; + + void after_transmit(std::function callback) override; + + void set_transmit_buffer(uint8_t* buffer, size_t size) override; + + void set_receive_buffer(uint8_t* buffer, size_t size) override; + + void _interrupt_service_routine(); + +private: + enum class State { + // Busy states + receiving = 0, // Receiving bytes from the master + transmitting, // Transmitting bytes to the master + + // 'idle' and above mean that the driver has finished + // whatever it was doing and is ready to do more work. + idle = 100, // Not in a transaction + aborted, // Transaction was aborted due to an error. + }; + + IMXRT_LPI2C_Registers* const port; + IMX_RT1060_I2CBase::Config& config; + volatile State state = State::idle; + + I2CBuffer rx_buffer = I2CBuffer(); + I2CBuffer tx_buffer = I2CBuffer(); + bool trailing_byte_sent = false; + + void (* isr)(); + std::function after_receive_callback = nullptr; + std::function before_transmit_callback = nullptr; + std::function after_transmit_callback = nullptr; + + // Called from within the ISR when we receive a Repeated START or STOP + void end_of_frame(); +}; + +extern IMX_RT1060_I2CSlave Slave; +extern IMX_RT1060_I2CSlave Slave1; +extern IMX_RT1060_I2CSlave Slave2; + +#endif //IMX_RT1060_I2C_DRIVER_H diff --git a/src/tests/raw_loopback.cpp b/src/tests/raw_loopback.cpp new file mode 100644 index 0000000..cd57dcc --- /dev/null +++ b/src/tests/raw_loopback.cpp @@ -0,0 +1,325 @@ +// Copyright © 2019 Richard Gemmell +// Released under the MIT License. See license.txt. (https://opensource.org/licenses/MIT) + +// Demonstrates use of the raw I2C driver as a slave receiver. +// Receives data from a master device. + +//#define I2C_RAW_LOOPBACK_EXAMPLE // Uncomment to build this example +#ifdef I2C_RAW_LOOPBACK_EXAMPLE + +#include +#include "i2c_driver.h" +#include "imx_rt1060/imx_rt1060_i2c_driver.h" + +// Blink the LED to make sure the Teensy hasn't hung +IntervalTimer blink_timer; +volatile bool led_high = false; +void blink_isr(); + +const bool stop = true; +const bool no_stop = false; + +// Master +I2CMaster& master = Master; +void log_master_transferred(uint8_t* buffer, size_t num_bytes); + +// Slave +const uint16_t slave_address = 0x002D; +I2CSlave& slave = Slave1; +void after_receive(int size); + +// Double receive buffers to hold data from master. +const size_t slave_rx_buffer_size = 5; +uint8_t slave_rx_buffer[slave_rx_buffer_size] = {}; +uint8_t slave_rx_buffer_2[slave_rx_buffer_size] = {}; +volatile size_t slave_bytes_received = 0; +void log_slave_message_received(); + +// Set up slave transmissions +const size_t slave_tx_buffer_size = 6; +uint8_t slave_tx_buffer[slave_tx_buffer_size] = {0x14, 0x15, 0x16, 0x17, 0x18, 0x19}; +void after_transmit(); +volatile bool after_transmit_received = false; +volatile bool buffer_underflow_detected = false; +void log_transmit_events(); + +void setup() { + // Turn the LED on + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, true); + // Create a timer to blink the LED + blink_timer.begin(blink_isr, 500000); + + // Configure I2C Slave + slave.after_receive(after_receive); + slave.set_receive_buffer(slave_rx_buffer, slave_rx_buffer_size); + slave.after_transmit(after_transmit); + slave.set_transmit_buffer(slave_tx_buffer, slave_tx_buffer_size); + slave.listen(slave_address); + + // Initialise the master + master.begin(1000 * 1000U); + + // Enable the serial port for debugging + Serial.begin(9600); + Serial.println("App Started"); +} + +uint32_t success_count = 0; +uint32_t fail_count = 0; + +void log_failure_count() { + Serial.print("App Master: "); + Serial.print(fail_count); + Serial.print(" failed. "); + Serial.print(success_count); + Serial.println(" succeeded. "); +} + +void finish(); +void test_master_read(); +void test_master_sequential_reads(); +void test_master_invalid_slave_address(); +void test_slave_read_buffer_overrun(); +void test_master_read_too_large(); +void test_master_write(); +void test_write_then_read(); + +bool done = false; +void loop() { + // Test cases + if(!done) { + test_master_read(); // Happy case reading bytes from the slave +// test_master_invalid_slave_address(); // Got the slave address wrong +// test_master_sequential_reads(); // Show that the user can hold the bus before doing another operation. +// test_slave_read_buffer_overrun(); // Slave should NACK if asked for too many bytes +// test_master_read_too_large(); // Master cannot read this many bytes in one go +// test_master_write(); // Happy case sending bytes to the slave +// test_write_then_read(); // The standard pattern of requesting a register value +// done = true; + } +// done = true; + + // Report slave transactions + log_transmit_events(); + if (slave_bytes_received) { + log_slave_message_received(); + slave_bytes_received = 0; + } + +// led_high = !led_high; +// digitalWrite(LED_BUILTIN, led_high); + delay(1000); +} + +void finish() { + elapsedMillis timeout; + while (timeout < 200) { + if (master.finished()){ + return; + } + } + Serial.println("Master: ERROR timed out waiting for transfer to finish"); +} + +void test_master_read() { + Serial.println(); + uint8_t read_buffer[6] = {}; + master.read_async(slave_address, read_buffer, sizeof(read_buffer), stop); + finish(); + + if (!master.has_error()) { + log_master_transferred(read_buffer, sizeof(read_buffer)); + success_count++; + log_failure_count(); + } else { + Serial.print("FAIL: App Master: Failed to read. Error: "); + Serial.println((int)master.error()); + fail_count++; + log_failure_count(); + } +} + +void test_master_sequential_reads() { + // Show that the user can hold the bus before doing another operation. + uint8_t read_buffer[6] = {}; + master.read_async(slave_address, read_buffer, sizeof(read_buffer), no_stop); + finish(); + + uint8_t read_buffer_2[2] = {}; + master.read_async(slave_address, read_buffer_2, sizeof(read_buffer_2), stop); + finish(); + + if (!master.has_error()) { + log_master_transferred(read_buffer, sizeof(read_buffer)); + log_master_transferred(read_buffer_2, sizeof(read_buffer_2)); + success_count++; + log_failure_count(); + } else { + Serial.print("FAIL: App Master: Failed to read. Error: "); + Serial.println((int)master.error()); + fail_count++; + log_failure_count(); + } +} + +void test_master_invalid_slave_address() { + // Use in invalid address to force an address NACK + uint8_t read_buffer[6] = {}; + master.read_async(0x11, read_buffer, sizeof(read_buffer), stop); + finish(); + if (master.has_error()) { + if (master.error() == I2CError::address_nak) { + Serial.println("PASS: App Master: Attempted to send to invalid address."); + } else { + Serial.print("FAIL: App Master: Attempted to send to invalid address. Expected address_nak but got: "); + Serial.println((int)master.error()); + } + } else { + Serial.println("FAIL: App Master: ERROR Unexpected success reading from invalid address."); + } +} + +void test_master_read_too_large() { + uint8_t read_buffer[257] = {}; + master.read_async(slave_address, read_buffer, sizeof(read_buffer), stop); + finish(); + if (master.has_error()) { + if (master.error() != I2CError::invalid_request) { + Serial.print("FAIL: App Master: Unexpected error attempting to read too many bytes. Expected invalid_request. Got : "); + Serial.println((int)master.error()); + } else { + Serial.println("PASS: App Master: Attempted to read too many bytes."); + } + } else { + Serial.print("FAIL: App Master: Unexpected success readying too many bytes. Error: "); + Serial.println((int) master.error()); + } +} + +// WARNING: Doesn't work at the moment because the Slave doesn't NACK +// it just sends dummy data instead. +void test_slave_read_buffer_overrun() { + // Ask for too many bytes to force a data NACK + uint8_t read_buffer[slave_tx_buffer_size+2] = {}; + master.read_async(slave_address, read_buffer, sizeof(read_buffer), stop); + finish(); + if (master.has_error()) { + Serial.print("PASS: App Master: Attempted to read too many bytes. Error: "); + Serial.println((int) master.error()); + } else { + Serial.println("FAIL: App Master: Unexpected success reading too many bytes."); + } + if (buffer_underflow_detected) { + Serial.println("PASS: App Slave: Buffer underflow reported correctly."); + } else { + Serial.println("FAIL: App Slave: Should have reported buffer underflow."); + } +} + +void log_master_transferred(uint8_t* buffer, size_t num_bytes) { + Serial.print("App Master: transferred "); + for (size_t i = 0; i < num_bytes; i++) { + Serial.print(buffer[i]); + if(i< num_bytes-1) { + Serial.print(", "); + } + } + Serial.println("."); +} + +void test_master_write() { + Serial.println(); + uint8_t write_buffer[5] = {0x40, 0x41, 0x42, 0x43, 0x44}; + master.write_async(slave_address, write_buffer, sizeof(write_buffer), stop); + finish(); + + if (!master.has_error()) { + log_master_transferred(write_buffer, sizeof(write_buffer)); + success_count++; + log_failure_count(); + } else { + Serial.print("FAIL: App Master: Failed to write. Error: "); + Serial.println((int) master.error()); + fail_count++; + log_failure_count(); + } +} + +void test_write_then_read() { + Serial.println(); + uint8_t write_buffer[1] = {0x40}; + master.write_async(slave_address, write_buffer, sizeof(write_buffer), no_stop); + finish(); + + if (master.has_error()) { + Serial.print("FAIL: App Master: Failed to write register. Error: "); + Serial.println((int) master.error()); + fail_count++; + log_failure_count(); + } + else { + uint8_t read_buffer[2] = {}; + master.read_async(slave_address, read_buffer, sizeof(read_buffer), stop); + finish(); + + if (!master.has_error()) { + log_master_transferred(read_buffer, sizeof(read_buffer)); + success_count++; + log_failure_count(); + } else { + Serial.print("FAIL: App Master: Failed to read from register. Error: "); + Serial.println((int) master.error()); + fail_count++; + log_failure_count(); + } + } +} + +void after_receive(int size) { + if (!slave_bytes_received) { + memcpy(slave_rx_buffer_2, slave_rx_buffer, size); + slave_bytes_received = size; + } +} + +void log_slave_message_received() { + if (slave.error() == I2CError::buffer_overflow) { + Serial.println("App Slave: Buffer Overflow. (Master sent too many bytes.)"); + } + Serial.print("App Slave: Slave received "); + Serial.print(slave_bytes_received); + Serial.print(" bytes: "); + for(size_t i=0; i < slave_bytes_received; i++) { + Serial.print(slave_rx_buffer_2[i]); + Serial.print(", "); + } + Serial.println(); +} + +void after_transmit() { + after_transmit_received = true; + if (slave.error() == I2CError::buffer_underflow) { + buffer_underflow_detected = true; + } + // Get ready for the next request + slave.set_transmit_buffer(slave_tx_buffer, slave_tx_buffer_size); +} + +void log_transmit_events() { + if (after_transmit_received) { +// Serial.println("App Slave: Transmission complete."); + after_transmit_received = false; + if (buffer_underflow_detected) { + Serial.println("App Slave: Buffer Underflow. (Master asked for too many bytes.)"); + buffer_underflow_detected = false; + } + } +} + +void blink_isr() { + led_high = !led_high; + digitalWrite(LED_BUILTIN, led_high); +} + +#endif \ No newline at end of file diff --git a/test/README b/test/README new file mode 100644 index 0000000..c3b0ed6 --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html diff --git a/test/test_register_i2c/test_i2c_device.cpp b/test/test_register_i2c/test_i2c_device.cpp new file mode 100644 index 0000000..45984df --- /dev/null +++ b/test/test_register_i2c/test_i2c_device.cpp @@ -0,0 +1,416 @@ +//#define TEST_I2C_DEVICE +#ifdef TEST_I2C_DEVICE + +#include +#include +#include +#include "i2c_device.h" + +struct TestBuffer { + bool _read = false; + uint16_t _address = 0; + uint8_t _buffer[4] = {}; + size_t _num_bytes = 0; + bool _send_stop = false; + + void set(bool read, uint16_t address, uint8_t* buffer, size_t num_bytes, bool send_stop) { + _read = read; + _address = address; + _num_bytes = num_bytes; + memcpy(&_buffer, buffer, _num_bytes); + _send_stop = send_stop; + } + + void assert_unused() { + assert_details(false, 0, 0, false); + uint8_t blank[] = {0, 0, 0, 0}; + TEST_ASSERT_EQUAL_MEMORY(blank, _buffer, sizeof(_buffer)); + } + + void assert_equals(bool read, uint16_t address, uint8_t buffer[], size_t num_bytes, bool send_stop) { + assert_details(read, address, num_bytes, send_stop); + TEST_ASSERT_EQUAL_MEMORY(buffer, _buffer, num_bytes); + } + + void assert_details(bool read, uint16_t address, size_t num_bytes, bool send_stop) { + TEST_ASSERT_EQUAL(read, _read); + TEST_ASSERT_EQUAL(address, _address); + TEST_ASSERT_EQUAL(send_stop, _send_stop); + } + + void reset() { + _read = false; + _address = 0; + memset(_buffer, 0, sizeof(_buffer)); + _num_bytes = 0; + _send_stop = false; + } +}; + +class DummyI2CMaster : public I2CMaster { +public: + void set_error(I2CError error) { + _error = error; + } + + void begin(uint32_t frequency) override { + }; + + void end() override { + }; + + bool finished() override { + return _finished; + }; + + void write_async(uint16_t address, uint8_t* buffer, size_t num_bytes, bool send_stop) override { + copy_to_next_buffer(false, address, buffer, num_bytes, send_stop); + }; + + void read_async(uint16_t address, uint8_t* buffer, size_t num_bytes, bool send_stop) override { + memcpy(buffer, read_data, num_bytes); + copy_to_next_buffer(true, address, buffer, num_bytes, send_stop); + }; + + void copy_to_next_buffer(bool read, uint16_t address, uint8_t* buffer, size_t num_bytes, bool send_stop){ + if(next_buffer < size_t(buffers)) { + buffers[next_buffer++].set(read, address, buffer, num_bytes, send_stop); + } + } + + void reset() { + _error = I2CError::ok; + next_buffer = 0; + _finished = false; + buffers[0].reset(); + buffers[1].reset(); + } + + TestBuffer buffers[2]; + size_t next_buffer = 0; + uint8_t read_data[8] = {0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}; + + bool _finished = false; +}; + +DummyI2CMaster dummy; +uint16_t address = 0x22; +I2CDevice device(dummy, address); +I2CDevice big_endian_device(dummy, address, BIG_ENDIAN); + +void setUp() { + dummy.reset(); +} + +void test_write() { + uint8_t reg = 0x12; + int16_t value = 0x01AB; + + bool success = device.write(reg, (uint8_t*)&value, sizeof(value), true); + + TEST_ASSERT_TRUE(success) + uint8_t expected_buffer[3] = {reg, 0xAB, 0x01}; + dummy.buffers[0].assert_equals(false, address, expected_buffer, sizeof(expected_buffer), true); +} + +void test_read_fails_to_send_register() { + uint8_t reg = 0xDD; + uint8_t rx_buffer[8] = {1, 2, 3, 4, 5, 6, 7, 8}; + dummy.set_error(I2CError::arbitration_lost); + + bool success = device.read(reg, rx_buffer, sizeof(rx_buffer), true); + + TEST_ASSERT_FALSE(success) + dummy.buffers[0].assert_equals(false, address, ®, sizeof(reg), false); + dummy.buffers[1].assert_unused(); + // Wipes target buffer if read fails. + uint8_t expected_rx_buffer[8] = {}; + TEST_ASSERT_EQUAL_MEMORY(expected_rx_buffer, rx_buffer, sizeof(rx_buffer)); +} + +void test_read() { + uint8_t reg = 0xDD; + uint8_t rx_buffer[8] = {}; + + bool success = device.read(reg, rx_buffer, sizeof(rx_buffer), true); + + TEST_ASSERT_TRUE(success) + dummy.buffers[0].assert_equals(false, address, ®, sizeof(reg), false); + dummy.buffers[1].assert_equals(true, address, rx_buffer, sizeof(rx_buffer), true); + uint8_t expected_rx_buffer[8] = {0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}; + TEST_ASSERT_EQUAL_MEMORY(expected_rx_buffer, rx_buffer, sizeof(rx_buffer)); +} + +void test_write_uint8t() { + uint8_t reg = 0x12; + uint8_t value = 0xDE; + + bool success = device.write(reg, value, false); + + TEST_ASSERT_TRUE(success) + uint8_t expected_buffer[2] = {reg, value}; + dummy.buffers[0].assert_equals(false, address, expected_buffer, sizeof(expected_buffer), false); +} + +void test_write_int8t() { + uint8_t reg = 0x10; + int8_t value = 0xDE; + + bool success = device.write(reg, value, true); + + TEST_ASSERT_TRUE(success) + uint8_t expected_buffer[2] = {reg, (uint8_t)value}; + dummy.buffers[0].assert_equals(false, address, expected_buffer, sizeof(expected_buffer), true); +} + +void test_write_uint16t() { + uint8_t reg = 0x12; + uint16_t value = 0xABDE; + + bool success = device.write(reg, value, true); + + TEST_ASSERT_TRUE(success) + uint8_t expected_buffer[3] = {reg, 0xDE, 0xAB}; + dummy.buffers[0].assert_equals(false, address, expected_buffer, sizeof(expected_buffer), true); +} + +void test_write_uint16t_big_endian() { + uint8_t reg = 0x12; + uint16_t value = 0xABDE; + + bool success = big_endian_device.write(reg, value, false); + + TEST_ASSERT_TRUE(success) + uint8_t expected_buffer[3] = {reg, 0xAB, 0xDE}; + dummy.buffers[0].assert_equals(false, address, expected_buffer, sizeof(expected_buffer), false); +} + +void test_write_int16t() { + uint8_t reg = 0x12; + int16_t value = 0xABDE; + + bool success = device.write(reg, value, true); + + TEST_ASSERT_TRUE(success) + uint8_t expected_buffer[3] = {reg, 0xDE, 0xAB}; + dummy.buffers[0].assert_equals(false, address, expected_buffer, sizeof(expected_buffer), true); +} + +void test_write_int16t_big_endian() { + uint8_t reg = 0x12; + int16_t value = 0xABDE; + + bool success = big_endian_device.write(reg, value, false); + + TEST_ASSERT_TRUE(success) + uint8_t expected_buffer[3] = {reg, 0xAB, 0xDE}; + dummy.buffers[0].assert_equals(false, address, expected_buffer, sizeof(expected_buffer), false); +} + +void test_write_uint32t() { + uint8_t reg = 0x12; + uint32_t value = 0xABDE1234; + + bool success = device.write(reg, value, true); + + TEST_ASSERT_TRUE(success) + uint8_t expected_buffer[5] = {reg, 0x34, 0x12, 0xDE, 0xAB}; + dummy.buffers[0].assert_equals(false, address, expected_buffer, sizeof(expected_buffer), true); +} + +void test_write_uint32t_big_endian() { + uint8_t reg = 0x12; + uint32_t value = 0xABDE1234; + + bool success = big_endian_device.write(reg, value, false); + + TEST_ASSERT_TRUE(success) + uint8_t expected_buffer[5] = {reg, 0xAB, 0xDE, 0x12, 0x34}; + dummy.buffers[0].assert_equals(false, address, expected_buffer, sizeof(expected_buffer), false); +} + +void test_write_int32t() { + uint8_t reg = 0x12; + int32_t value = 0xABDE1234; + + bool success = device.write(reg, value, true); + + TEST_ASSERT_TRUE(success) + uint8_t expected_buffer[5] = {reg, 0x34, 0x12, 0xDE, 0xAB}; + dummy.buffers[0].assert_equals(false, address, expected_buffer, sizeof(expected_buffer), true); +} + +void test_write_int32t_big_endian() { + uint8_t reg = 0x12; + int32_t value = 0xABDE1234; + + bool success = big_endian_device.write(reg, value, false); + + TEST_ASSERT_TRUE(success) + uint8_t expected_buffer[5] = {reg, 0xAB, 0xDE, 0x12, 0x34}; + dummy.buffers[0].assert_equals(false, address, expected_buffer, sizeof(expected_buffer), false); +} + +void test_read_uin8t() { + uint8_t reg = 0x12; + uint8_t value = 0; + + bool success = device.read(reg, &value, true); + + TEST_ASSERT_TRUE(success) + dummy.buffers[0].assert_equals(false, address, ®, sizeof(reg), false); + dummy.buffers[1].assert_equals(true, address, &value, sizeof(value), true); + TEST_ASSERT_EQUAL(0x08, value); +} + +void test_read_int8t() { + uint8_t reg = 0x12; + int8_t value = 0; + + bool success = device.read(reg, &value, true); + + TEST_ASSERT_TRUE(success) + dummy.buffers[0].assert_equals(false, address, ®, sizeof(reg), false); + dummy.buffers[1].assert_equals(true, address, (uint8_t*)&value, sizeof(value), true); + TEST_ASSERT_EQUAL((int8_t)0x08, value); +} + +void test_read_uin16t() { + uint8_t reg = 0x12; + uint16_t value = 0; + + bool success = device.read(reg, &value, true); + + TEST_ASSERT_TRUE(success) + dummy.buffers[0].assert_equals(false, address, ®, sizeof(reg), false); + dummy.buffers[1].assert_equals(true, address, (uint8_t*)&value, sizeof(value), true); + TEST_ASSERT_EQUAL(0x0908, value); +} + +void test_read_uin16t_big_endian() { + uint8_t reg = 0x12; + uint16_t value = 0; + + bool success = big_endian_device.read(reg, &value, true); + + TEST_ASSERT_TRUE(success) + dummy.buffers[0].assert_equals(false, address, ®, sizeof(reg), false); + dummy.buffers[1].assert_details(true, address, sizeof(value), true); + TEST_ASSERT_EQUAL(0x0809, value); +} + +void test_read_in16t() { + uint8_t reg = 0x12; + int16_t value = 0; + + bool success = device.read(reg, &value, true); + + TEST_ASSERT_TRUE(success) + dummy.buffers[0].assert_equals(false, address, ®, sizeof(reg), false); + dummy.buffers[1].assert_equals(true, address, (uint8_t*)&value, sizeof(value), true); + TEST_ASSERT_EQUAL(0x0908, value); +} + +void test_read_in16t_big_endian() { + uint8_t reg = 0x16; + int16_t value = 0; + + bool success = big_endian_device.read(reg, &value, false); + + TEST_ASSERT_TRUE(success) + dummy.buffers[0].assert_equals(false, address, ®, sizeof(reg), false); + dummy.buffers[1].assert_details(true, address, sizeof(value), false); + TEST_ASSERT_EQUAL(0x0809, value); +} + +void test_read_uin32t() { + uint8_t reg = 0x12; + uint32_t value = 0; + + bool success = device.read(reg, &value, true); + + TEST_ASSERT_TRUE(success) + dummy.buffers[0].assert_equals(false, address, ®, sizeof(reg), false); + dummy.buffers[1].assert_equals(true, address, (uint8_t*)&value, sizeof(value), true); + TEST_ASSERT_EQUAL(0x0B0A0908, value); +} + +void test_read_uin32t_big_endian() { + uint8_t reg = 0x12; + uint32_t value = 0; + + bool success = big_endian_device.read(reg, &value, true); + + TEST_ASSERT_TRUE(success) + dummy.buffers[0].assert_equals(false, address, ®, sizeof(reg), false); + dummy.buffers[1].assert_details(true, address, sizeof(value), true); + TEST_ASSERT_EQUAL(0x08090A0B, value); +} + +void test_read_in32t() { + uint8_t reg = 0x12; + int32_t value = 0; + + bool success = device.read(reg, &value, true); + + TEST_ASSERT_TRUE(success) + dummy.buffers[0].assert_equals(false, address, ®, sizeof(reg), false); + dummy.buffers[1].assert_equals(true, address, (uint8_t*)&value, sizeof(value), true); + TEST_ASSERT_EQUAL(0x0B0A0908, value); +} + +void test_read_in32t_big_endian() { + uint8_t reg = 0x12; + int32_t value = 0; + + bool success = big_endian_device.read(reg, &value, true); + + TEST_ASSERT_TRUE(success) + dummy.buffers[0].assert_equals(false, address, ®, sizeof(reg), false); + dummy.buffers[1].assert_details(true, address, sizeof(value), true); + TEST_ASSERT_EQUAL(0x08090A0B, value); +} + +void setup() { + pinMode(13, OUTPUT); + delay(1000); + + UNITY_BEGIN(); + RUN_TEST(test_write); + RUN_TEST(test_read_fails_to_send_register); + RUN_TEST(test_read); + + RUN_TEST(test_write_uint8t); + RUN_TEST(test_write_int8t); + RUN_TEST(test_write_uint16t); + RUN_TEST(test_write_uint16t_big_endian); + RUN_TEST(test_write_int16t); + RUN_TEST(test_write_int16t_big_endian); + RUN_TEST(test_write_uint32t); + RUN_TEST(test_write_uint32t_big_endian); + RUN_TEST(test_write_int32t); + RUN_TEST(test_write_int32t_big_endian); + + RUN_TEST(test_read_uin8t); + RUN_TEST(test_read_int8t); + RUN_TEST(test_read_uin16t); + RUN_TEST(test_read_uin16t_big_endian); + RUN_TEST(test_read_in16t); + RUN_TEST(test_read_in16t_big_endian); + RUN_TEST(test_read_uin32t); + RUN_TEST(test_read_uin32t_big_endian); + RUN_TEST(test_read_in32t); + RUN_TEST(test_read_in32t_big_endian); + + UNITY_END(); +} + +void loop() { +// Serial.println("Loop"); + digitalWrite(13, HIGH); + delay(100); + digitalWrite(13, LOW); + delay(900); +} + +#endif //TEST_I2C_DEVICE \ No newline at end of file diff --git a/test/test_register_i2c/test_i2c_register_slave.cpp b/test/test_register_i2c/test_i2c_register_slave.cpp new file mode 100644 index 0000000..d161e8d --- /dev/null +++ b/test/test_register_i2c/test_i2c_register_slave.cpp @@ -0,0 +1,461 @@ +//#define TEST_I2C_REGISTER_SLAVE +#ifdef TEST_I2C_REGISTER_SLAVE + +#include +#include +#include +#include "i2c_register_slave.h" + +// HACK to solve a linker problem. +// I guess exceptions are enabled in the Test build +unsigned __exidx_start; +unsigned __exidx_end; + +class DummyI2CSlave : public I2CSlave { +public: + void listen(uint16_t slave_address) override { + address = slave_address; + }; + + void stop_listening() override {}; + + inline void after_receive(std::function callback) override { + after_receive_callback = callback; + } + + void before_transmit(std::function callback) override { + }; + + void after_transmit(std::function callback) override { + after_transmit_callback = callback; + }; + + void set_transmit_buffer(uint8_t* buffer, size_t size) override { + latest_tx_buffer = buffer; + latest_tx_buffer_size = size; + }; + + void set_receive_buffer(uint8_t* buffer, size_t size) override { + latest_rx_buffer = buffer; + latest_rx_buffer_size = size; + } + + void write(uint8_t reg_num, uint8_t* buffer, size_t len) { + if (latest_rx_buffer) { + latest_rx_buffer[0] = reg_num; + memcpy(latest_rx_buffer+1, buffer, min(len, latest_rx_buffer_size-1)); + if(after_receive_callback) { + after_receive_callback(len+1); + } + } + } + + void write_reg_number(uint8_t reg_num) { + if (latest_rx_buffer != nullptr && latest_rx_buffer_size > 0) { + latest_rx_buffer[0] = reg_num; + if(after_receive_callback) { + after_receive_callback(sizeof(uint8_t)); + } + } + } + + void write_value(uint8_t* buffer, size_t len) { + if (latest_rx_buffer) { + memcpy(latest_rx_buffer, buffer, min(len, latest_rx_buffer_size)); + if(after_receive_callback) { + after_receive_callback(len); + } + } + } + + void read_value(uint8_t* buffer, size_t len) { + if (latest_tx_buffer) { + memcpy(buffer, latest_tx_buffer, min(len, latest_tx_buffer_size)); + if(after_transmit_callback) { + after_transmit_callback(); + } + } + } + + void reset() { + latest_rx_buffer = nullptr; + latest_rx_buffer_size = 0; + latest_tx_buffer = nullptr; + latest_tx_buffer_size = 0; + } + + uint16_t address = 0; + uint8_t* latest_rx_buffer = nullptr; + size_t latest_rx_buffer_size = 0; + uint8_t* latest_tx_buffer = nullptr; + size_t latest_tx_buffer_size = 0; + +private: + std::function after_receive_callback = nullptr; +// std::function before_transmit_callback = nullptr; + std::function after_transmit_callback = nullptr; +}; + +DummyI2CSlave dummy; +uint16_t address = 0xDD; +uint8_t settings[4] = {0x00, 0x00, 0x00, 0x00}; +uint8_t filled_settings[4] = {0xAA, 0xBB, 0xCC, 0xDD}; +uint8_t blank_read_only[4] = {0x00, 0x00, 0x00, 0x00}; +uint8_t read_only[4] = {0x0A, 0x0B, 0x0C, 0x0D}; + +void setUp() { + dummy.reset(); + memset(settings, 0, sizeof(settings)); +} + +void test_listen_calls_listen_on_driver() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + TEST_ASSERT_EQUAL(address, dummy.address); +} + +void test_master_can_write_to_mutable_register() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint16_t value = 0xBBAA; + dummy.write_reg_number(0x01); + dummy.write_value((uint8_t*)&value, sizeof(value)); + + uint8_t expected[sizeof(settings)] = {0x00, 0xAA, 0xBB, 0x00}; + TEST_ASSERT_EQUAL_MEMORY(expected, settings, sizeof(settings)); +} + +void test_master_can_write_to_multiple_registers() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint8_t value_1 = 0xAA; + dummy.write_reg_number(0x01); + dummy.write_value((uint8_t*)&value_1, sizeof(value_1)); + + uint8_t value_2 = 0xFF; + dummy.write_reg_number(0x03); + dummy.write_value((uint8_t*)&value_2, sizeof(value_2)); + + uint8_t expected[sizeof(settings)] = {0x00, 0xAA, 0x00, 0xFF}; + TEST_ASSERT_EQUAL_MEMORY(expected, settings, sizeof(settings)); +} + +void test_master_cannot_write_to_non_existent_register() { + uint8_t big_settings[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + size_t claimed_settings_size = 2; + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, big_settings, claimed_settings_size, read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint8_t reg = 2; // beyond the end of the claimed size of big_settings + uint32_t value = 0xAABBCCDD; + dummy.write_reg_number(reg); + dummy.write_value((uint8_t*)&value, sizeof(value)); + + uint8_t expected[sizeof(big_settings)] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + TEST_ASSERT_EQUAL_MEMORY(expected, big_settings, sizeof(big_settings)); +} + +void test_master_cannot_write_too_many_bytes_to_a_register() { + uint8_t big_settings[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + size_t claimed_settings_size = 2; + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, big_settings, claimed_settings_size, read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint8_t reg = 0x01; // beyond the end of the claimed size of big_settings + uint32_t value = 0xAABBCCDD; + dummy.write_reg_number(reg); + dummy.write_value((uint8_t*)&value, sizeof(value)); + + uint8_t expected[sizeof(big_settings)] = {0x00, 0xDD, 0x00, 0x00, 0x00, 0x00}; + TEST_ASSERT_EQUAL_MEMORY(expected, big_settings, sizeof(big_settings)); +} + +void test_master_ignores_repeated_write_to_mutable_register() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint8_t value_1 = 0xAA; + dummy.write_reg_number(0x01); + dummy.write_value((uint8_t*)&value_1, sizeof(value_1)); + + // Should ignore this write because we haven't specified the register. + uint8_t value_2 = 0xFF; + dummy.write_value((uint8_t*)&value_2, sizeof(value_2)); + + uint8_t expected[sizeof(settings)] = {0x00, 0xAA, 0x00, 0x00}; + TEST_ASSERT_EQUAL_MEMORY(expected, settings, sizeof(settings)); +} + +void test_master_can_write_register_in_single_transaction() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint16_t value = 0xBBAA; + dummy.write(0x01, (uint8_t*)&value, sizeof(value)); + + uint8_t expected[sizeof(settings)] = {0x00, 0xAA, 0xBB, 0x00}; + TEST_ASSERT_EQUAL_MEMORY(expected, settings, sizeof(settings)); +} + +void test_master_can_write_to_multiple_registers_with_single_transactions() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint8_t value_1 = 0xAA; + dummy.write(0x01, (uint8_t*)&value_1, sizeof(value_1)); + + uint8_t value_2 = 0xFF; + dummy.write(0x03, (uint8_t*)&value_2, sizeof(value_2)); + + uint8_t expected[sizeof(settings)] = {0x00, 0xAA, 0x00, 0xFF}; + TEST_ASSERT_EQUAL_MEMORY(expected, settings, sizeof(settings)); +} + +void test_ignores_single_transaction_write_to_non_existent_register() { + uint8_t big_settings[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + size_t claimed_settings_size = 2; + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, big_settings, claimed_settings_size, read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint8_t reg = 2; // beyond the end of the claimed size of big_settings + uint32_t value = 0xAABBCCDD; + dummy.write(reg, (uint8_t*)&value, sizeof(value)); + + uint8_t expected[sizeof(big_settings)] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + TEST_ASSERT_EQUAL_MEMORY(expected, big_settings, sizeof(big_settings)); +} + +void test_master_cannot_write_too_many_bytes_to_a_register_with_single_transaction() { + uint8_t big_settings[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + size_t claimed_settings_size = 2; + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, big_settings, claimed_settings_size, read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint8_t reg = 0x01; // beyond the end of the claimed size of big_settings + uint32_t value = 0xAABBCCDD; + dummy.write(reg, (uint8_t*)&value, sizeof(value)); + + uint8_t expected[sizeof(big_settings)] = {0x00, 0xDD, 0x00, 0x00, 0x00, 0x00}; + TEST_ASSERT_EQUAL_MEMORY(expected, big_settings, sizeof(big_settings)); +} + +void test_master_can_read_from_mutable_register() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, filled_settings, sizeof(filled_settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint16_t value = 0; + dummy.write_reg_number(0x01); + dummy.read_value((uint8_t*)&value, sizeof(value)); + + TEST_ASSERT_EQUAL(0xCCBB, value); +} + +void test_master_can_read_from_multiple_mutable_registers() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, filled_settings, sizeof(filled_settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint16_t value_1 = 0; + dummy.write_reg_number(0x01); + dummy.read_value((uint8_t*)&value_1, sizeof(value_1)); + TEST_ASSERT_EQUAL(0xCCBB, value_1); + + uint8_t value_2 = 0; + dummy.write_reg_number(0x00); + dummy.read_value((uint8_t*)&value_2, sizeof(value_2)); + TEST_ASSERT_EQUAL(0xAA, value_2); +} + +void test_master_cannot_read_from_non_existent_register() { + uint8_t long_read_only[] = {0x0A, 0x0B, 0x0C, 0x0D, 0x11, 0x11, 0x11, 0x11}; + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), long_read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint8_t reg = sizeof(settings) + sizeof(read_only); // beyond the end of the claimed size of buffers + dummy.write_reg_number(reg); + uint8_t value = 0; + dummy.read_value((uint8_t*)&value, sizeof(value)); + + TEST_ASSERT_EQUAL(0x00, value); +} + +void test_master_cannot_read_too_many_bytes_from_a_register() { + size_t claimed_settings_size = 2; + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, filled_settings, claimed_settings_size, read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint32_t value = 0; + dummy.write_reg_number(0x0); + dummy.read_value((uint8_t*)&value, sizeof(value)); + + TEST_ASSERT_EQUAL(0x0000BBAA, value); +} + +void test_master_ignores_repeated_read_of_mutable_register() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, filled_settings, sizeof(filled_settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint8_t reg_1 = 0x01; + uint16_t value_1 = 0; + dummy.write_reg_number(reg_1); + dummy.read_value((uint8_t*)&value_1, sizeof(value_1)); + TEST_ASSERT_EQUAL(0xCCBB, value_1); + + // Must not populated value_2 without sending another register number first + uint16_t value_2 = 0; + dummy.read_value((uint8_t*)&value_2, sizeof(value_2)); + TEST_ASSERT_EQUAL(0x00, value_2); +} + +void test_master_cannot_write_to_readonly_register() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), blank_read_only, sizeof(blank_read_only)); + reg_slave.listen(address); + + uint8_t reg = sizeof(settings) + 1; // First byte of read_only buffer + uint8_t value = 0xAA; + dummy.write_reg_number(reg); + dummy.write_value((uint8_t*)&value, sizeof(value)); + + uint8_t expected[sizeof(blank_read_only)] = {0x00, 0x00, 0x00, 0x00}; + TEST_ASSERT_EQUAL_MEMORY(expected, blank_read_only, sizeof(blank_read_only)); +} + +void test_master_can_read_from_readonly_register() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint16_t value = 0; + dummy.write_reg_number(sizeof(settings) + 2); + dummy.read_value((uint8_t*)&value, sizeof(value)); + + TEST_ASSERT_EQUAL(0x0D0C, value); +} + + +void test_master_can_read_from_multiple_readonly_registers() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint8_t value_1 = 0; + dummy.write_reg_number(sizeof(settings) + 3); + dummy.read_value((uint8_t*)&value_1, sizeof(value_1)); + TEST_ASSERT_EQUAL(0x0D, value_1); + + uint8_t value_2 = 0; + dummy.write_reg_number(sizeof(settings)); + dummy.read_value((uint8_t*)&value_2, sizeof(value_2)); + TEST_ASSERT_EQUAL(0x0A, value_2); +} + +void test_master_cannot_read_too_many_bytes_from_a_readonly_register() { + uint8_t long_read_only[] = {0x0A, 0x0B, 0x0C, 0x0D, 0x11, 0x11, 0x11, 0x11}; + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), long_read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint32_t value = 0; + dummy.write_reg_number(sizeof(settings) + 2); + dummy.read_value((uint8_t*)&value, sizeof(value)); + + TEST_ASSERT_EQUAL(0x00000D0C, value); +} + +void test_master_ignores_repeated_read_of_readonly_register() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint8_t reg_1 = sizeof(settings) + 0x01; + uint16_t value_1 = 0; + dummy.write_reg_number(reg_1); + dummy.read_value((uint8_t*)&value_1, sizeof(value_1)); + TEST_ASSERT_EQUAL(0x0C0B, value_1); + + // Must not populated value_2 without sending another register number first + uint16_t value_2 = 0; + dummy.read_value((uint8_t*)&value_2, sizeof(value_2)); + TEST_ASSERT_EQUAL(0x00, value_2); +} + +void test_app_receives_callback_after_read() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint8_t callback_reg_num = 0; + auto callback = [&callback_reg_num](uint8_t the_register) { + callback_reg_num = the_register; + }; + reg_slave.after_read(callback); + + uint8_t reg_1 = sizeof(settings) + 0x01; + uint16_t value_1 = 0; + dummy.write_reg_number(reg_1); + dummy.read_value((uint8_t*)&value_1, sizeof(value_1)); + + TEST_ASSERT_EQUAL(reg_1, callback_reg_num); +} + +void test_app_receives_callback_after_write() { + I2CRegisterSlave reg_slave = I2CRegisterSlave(dummy, settings, sizeof(settings), read_only, sizeof(read_only)); + reg_slave.listen(address); + + uint8_t callback_reg_num = 0; + auto callback = [&callback_reg_num](uint8_t the_register) { + callback_reg_num = the_register; + }; + reg_slave.after_write(callback); + + uint8_t reg = 0x0A; + dummy.write_reg_number(reg); + TEST_ASSERT_EQUAL(0, callback_reg_num); // Shouldn't have been called yet. + + uint16_t value = 0xFF; + dummy.write_value((uint8_t*)&value, sizeof(value)); + TEST_ASSERT_EQUAL(reg, callback_reg_num); +} + +void setup() { + pinMode(13, OUTPUT); + delay(2000); + + UNITY_BEGIN(); + + RUN_TEST(test_listen_calls_listen_on_driver); + + RUN_TEST(test_master_can_write_to_mutable_register); + RUN_TEST(test_master_can_write_to_multiple_registers); + RUN_TEST(test_master_cannot_write_to_non_existent_register); + RUN_TEST(test_master_cannot_write_too_many_bytes_to_a_register); + RUN_TEST(test_master_ignores_repeated_write_to_mutable_register); + + RUN_TEST(test_master_can_write_register_in_single_transaction); + RUN_TEST(test_master_can_write_to_multiple_registers_with_single_transactions); + RUN_TEST(test_ignores_single_transaction_write_to_non_existent_register); + RUN_TEST(test_master_cannot_write_too_many_bytes_to_a_register_with_single_transaction); + + RUN_TEST(test_master_can_read_from_mutable_register); + RUN_TEST(test_master_can_read_from_multiple_mutable_registers); + RUN_TEST(test_master_cannot_read_from_non_existent_register); + RUN_TEST(test_master_cannot_read_too_many_bytes_from_a_register); + RUN_TEST(test_master_ignores_repeated_read_of_mutable_register); + + RUN_TEST(test_master_cannot_write_to_readonly_register); + RUN_TEST(test_master_can_read_from_readonly_register); + RUN_TEST(test_master_can_read_from_multiple_readonly_registers); + RUN_TEST(test_master_cannot_read_too_many_bytes_from_a_readonly_register); + RUN_TEST(test_master_ignores_repeated_read_of_readonly_register); + + RUN_TEST(test_app_receives_callback_after_read); + RUN_TEST(test_app_receives_callback_after_write); + + UNITY_END(); +} + +void loop() { + digitalWrite(13, HIGH); + delay(100); + digitalWrite(13, LOW); + delay(900); +} + +#endif //TEST_I2C_REGISTER_SLAVE \ No newline at end of file