diff --git a/CMakeLists.txt b/CMakeLists.txt index d603074..d3f9ce7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ libhal_test_and_make_library( LIBRARY_NAME libhal-soft SOURCES + src/i2c_bit_bang.cpp src/rc_servo.cpp src/i2c_minimum_speed.cpp src/adc_mux.cpp diff --git a/conanfile.py b/conanfile.py index 67cf2af..96fba0f 100644 --- a/conanfile.py +++ b/conanfile.py @@ -59,7 +59,7 @@ def build_requirements(self): def requirements(self): self.requires("libhal/[^3.0.0]", transitive_headers=True) - self.requires("libhal-util/[^4.0.0]") + self.requires("libhal-util/[^4.0.1]") def layout(self): cmake_layout(self) diff --git a/demos/CMakeLists.txt b/demos/CMakeLists.txt new file mode 100755 index 0000000..7c6621f --- /dev/null +++ b/demos/CMakeLists.txt @@ -0,0 +1,29 @@ +# Copyright 2024 Khalil Estell +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +cmake_minimum_required(VERSION 3.15) + +project(demos LANGUAGES CXX) + +libhal_build_demos( + DEMOS + i2c_bit_bang + + PACKAGES + libhal-soft + libhal-stm-imu + + LINK_LIBRARIES + libhal::soft + libhal::stm-imu +) diff --git a/demos/applications/i2c_bit_bang.cpp b/demos/applications/i2c_bit_bang.cpp new file mode 100644 index 0000000..b07360e --- /dev/null +++ b/demos/applications/i2c_bit_bang.cpp @@ -0,0 +1,50 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include + +#include "../hardware_map.hpp" + +void application(hardware_map_t& p_map) +{ + using namespace std::chrono_literals; + using namespace hal::literals; + + auto& clock = *p_map.clock; + auto& console = *p_map.console; + auto& scl = *p_map.scl; + auto& sda = *p_map.sda; + + hal::print(console, "Starting lis3dhtr_i2c Application...\n"); + hal::delay(clock, 50ms); + + hal::i2c_bit_bang i2c_bit_bang(scl, sda, clock, hal::i2c_bit_bang::bus_info{}); + i2c_bit_bang.configure(hal::i2c::settings{.clock_rate = 100.0_kHz}); + + hal::stm_imu::lis3dhtr_i2c lis(i2c_bit_bang); + + while (true) { + hal::delay(clock, 500ms); + auto acceleration = lis.read(); + hal::print<128>(console, + "Scale: 2g \t x = %fg, y = %fg, z = %fg \n", + acceleration.x, + acceleration.y, + acceleration.z); + } +} diff --git a/demos/conanfile.py b/demos/conanfile.py new file mode 100755 index 0000000..7c0a386 --- /dev/null +++ b/demos/conanfile.py @@ -0,0 +1,26 @@ +# Copyright 2024 Khalil Estell +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from conan import ConanFile + + +class demos(ConanFile): + python_requires = "libhal-bootstrap/[^1.0.0]" + python_requires_extend = "libhal-bootstrap.demo" + + def requirements(self): + bootstrap = self.python_requires["libhal-bootstrap"] + bootstrap.module.add_demo_requirements(self) + self.requires("libhal-soft/[>=4.0.0]") + self.requires("libhal-stm-imu/[>=1.0.0]") diff --git a/demos/hardware_map.hpp b/demos/hardware_map.hpp new file mode 100644 index 0000000..f419748 --- /dev/null +++ b/demos/hardware_map.hpp @@ -0,0 +1,34 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +struct hardware_map_t +{ + hal::serial* console; + hal::output_pin* scl; + hal::output_pin* sda; + hal::steady_clock* clock; + hal::callback reset; +}; + +// Application function must be implemented by one of the compilation units +// (.cpp) files. +void application(hardware_map_t& p_map); +hardware_map_t initialize_platform(); diff --git a/demos/main.cpp b/demos/main.cpp new file mode 100755 index 0000000..5efabd8 --- /dev/null +++ b/demos/main.cpp @@ -0,0 +1,32 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "hardware_map.hpp" + +hardware_map_t hardware_map{}; + +int main() +{ + try { + hardware_map = initialize_platform(); + } catch (...) { + hal::halt(); + } + + application(hardware_map); + hardware_map.reset(); + return 0; +} diff --git a/demos/platforms/lpc4074.cpp b/demos/platforms/lpc4074.cpp new file mode 100644 index 0000000..4ed140a --- /dev/null +++ b/demos/platforms/lpc4074.cpp @@ -0,0 +1,56 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "../hardware_map.hpp" + +hardware_map_t initialize_platform() +{ + using namespace hal::literals; + + // Set the MCU to the maximum clock speed + hal::lpc40::maximum(12.0_MHz); + + static hal::cortex_m::dwt_counter counter( + hal::lpc40::get_frequency(hal::lpc40::peripheral::cpu)); + + static std::array receive_buffer{}; + static hal::lpc40::uart uart0(0, + receive_buffer, + hal::serial::settings{ + .baud_rate = 38400, + }); + + + static hal::lpc40::output_pin scl(1, 15); + static hal::lpc40::output_pin sda(1, 23); + + return hardware_map_t{ + .console = &uart0, + .output_pin0 = &scl, + .output_pin1 = &sda, + .clock = &counter, + .reset = []() { hal::cortex_m::reset(); }, + }; +} diff --git a/demos/platforms/lpc4078.cpp b/demos/platforms/lpc4078.cpp new file mode 100644 index 0000000..2288f3a --- /dev/null +++ b/demos/platforms/lpc4078.cpp @@ -0,0 +1,53 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#include +#include +#include +#include + +#include "../hardware_map.hpp" + +hardware_map_t initialize_platform() +{ + using namespace hal::literals; + + // Set the MCU to the maximum clock speed + hal::lpc40::maximum(12.0_MHz); + + static hal::cortex_m::dwt_counter counter( + hal::lpc40::get_frequency(hal::lpc40::peripheral::cpu)); + + static std::array receive_buffer{}; + static hal::lpc40::uart uart0(0, + receive_buffer, + hal::serial::settings{ + .baud_rate = 38400, + }); + + static hal::lpc40::output_pin scl(1, 15); // scl + static hal::lpc40::output_pin sda(1, 23); // sda + + return hardware_map_t{ + .console = &uart0, + .scl = &scl, + .sda = &sda, + .clock = &counter, + .reset = []() { hal::cortex_m::reset(); }, + }; +} diff --git a/demos/seleae_captures/12c_bit_bang_capture.sal b/demos/seleae_captures/12c_bit_bang_capture.sal new file mode 100644 index 0000000..cbee2a4 Binary files /dev/null and b/demos/seleae_captures/12c_bit_bang_capture.sal differ diff --git a/include/libhal-soft/i2c_bit_bang.hpp b/include/libhal-soft/i2c_bit_bang.hpp new file mode 100644 index 0000000..dd4573d --- /dev/null +++ b/include/libhal-soft/i2c_bit_bang.hpp @@ -0,0 +1,181 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +namespace hal { +/** + * @brief A bit bang implementation for i2c. This implementation of i2c only + * needs 2 gpio's to work correctly. In its current implementation we only + * support single controller multiple peripheral. But have intentions of + * supporting multiple controller soon. Additionally, it's important to note that bit bang + * is a best try implementation of i2c. As in it will do it's best to reach the requested frequency but will typically be slower then requested + * with brief testing it seems that the error is about 2us per period for the lpc4078 , so you can try to account for this error with the frequency you set + * for example, achieving the correct period for 100kHz would result in setting slightly more then a 125kHz frequency. Additionally, the fastest frequency this bit bang can handle is around 200kHz. + */ +class i2c_bit_bang : public i2c +{ +public: + + /// holds all of the information for an i2c bus + struct bus_info + { + float duty_cycle = 0.5f; + }; + + /** + * @brief Construct a new i2c bit bang object + * + * @param p_scl an output pin which will imitate the scl pin in a normal i2c + * hardware interface + * @param p_sda an output pin which will imitate the sda pin in a normal i2c + * hardwar interface + * @param p_steady_clock a steady clock that should have a higher frequency then the configured frequency for the bit bang + * @param p_bus the bus information which will be used which configuring the bit bang interface + */ + + i2c_bit_bang(output_pin& p_scl, + output_pin& p_sda, + steady_clock& p_steady_clock, + bus_info p_bus); + + void driver_configure(const settings& p_settings) override; + + virtual void driver_transaction( + hal::byte p_address, + std::span p_data_out, + std::span p_data_in, + function_ref p_timeout) override; + +private: + /** + * @brief this function will send the start condition and it will always pull the pins high before sending the start condition + */ + void send_start_condition(); + + /** + * @brief this function will send the stop condition while also making sure the sda pin is pulled low before sending the stop condition + * + */ + void send_stop_condition(); + + /** + * @brief this function will go through the steps of writing the address of the peripheral the controller wishes to speak to while also ensuring the data written is acknowledged + * + * @param p_address the address of the peripheral, configured with the read/write bit already that the controller is requesting to talk to + * @param p_timeout a timeout function which is primarily used for clock stretching to ensure the peripheral doesn't hold the line too long + * + * @throws hal::io_error when the address written to the bus was not acknowledged + */ + void write_address(hal::byte p_address, + function_ref p_timeout); + + /** + * @brief this function will write the entire contents of the span out to the bus while making sure all data gets acknowledged + * + * @param p_data_out this is a span of bytes which will be written to the bus + * @param p_timeout a timeout function which is primarily used for clock stretching to ensure the peripheral doesn't hold the line too long + */ + void write(std::span p_data_out, + function_ref p_timeout); + + /** + * @brief this function will handle writing a singular byte each call while also retrieving the acknowledge bits + * + * @param p_byte_to_write this is the byte that will be written to the bus + * @param p_timeout a timeout function which is primarily used for clock stretching to ensure the peripheral doesn't hold the line too long + * @return bool - true when the byte written was ack'd and false when it was nack'd + */ + bool write_byte(hal::byte p_byte_to_write, + function_ref p_timeout); + + /** + * @brief this function will write a single bit at a time, dealing with simulating the clock and the clock stretching feature + * + * @param p_bit_to_write the bit which will be written on the bus + * @param p_timeout a timeout function which is primarily used for clock stretching to ensure the peripheral doesn't hold the line too long + */ + void write_bit(hal::byte p_bit_to_write, + function_ref p_timeout); + + /** + * @brief this function will read in as many bytes as allocated inside of the span while also acking or nacking the data + * + * @param p_data_in a span which will be filled with the bytes that will be read from the bus + * @param p_timeout a timeout function which is primarily used for clock stretching to ensure the peripheral doesn't hold the line too long + */ + void read(std::span p_data_in, + function_ref p_timeout); + + /** + * @brief this function is responsible for reading a byte at a time off the bus and also creating the byte from bits + * + * @return hal::byte the byte that has been read off of the bus + */ + hal::byte read_byte(); + + /** + * @brief this function is responsible for reading a single bit at a time off of the bus while also managing the clock line. It will release (pull sda high) every time it is called + * + * @return hal::byte + */ + hal::byte read_bit(); + + /** + * @brief this function is a high speed version of the hal::delay function which operates on ticks rather then a time duration + * where hal::delay provides delays up to 17us, this function will provide delays up to about 2.7us making it optimal for high accuracy delay + * + * @param ticks the amount of ticks this function will delay for, this should be based on the frequency of the clock passed into the bit_bang class + */ + [[gnu::always_inline]] inline void high_speed_delay(uint64_t ticks); + + /** + * @brief an output pin which will mimic the behavior of an I2C scl pin + */ + output_pin* m_scl; + /** + * @brief an output pin which will mimic the bahavior of an I2C sda pin + */ + output_pin* m_sda; + /** + * @brief a steady_clock which will be used to correctly time the different + * frequencies + */ + steady_clock* m_clock; + + /** + * @brief the time that scl will be held high for + */ + uint64_t m_scl_high_ticks; + /** + * @brief the time that scl will be held low for + */ + uint64_t m_scl_low_ticks; + + /** + * @brief the amount of ticks that the uptime function takes to run, we store this so we save computation time in our high speed delay function + */ + uint32_t m_uptime_ticks; + + /** + * @brief all the information that the bus will need to operate on + */ + bus_info m_bus; +}; +} // namespace hal diff --git a/src/i2c_bit_bang.cpp b/src/i2c_bit_bang.cpp new file mode 100644 index 0000000..af1c1e4 --- /dev/null +++ b/src/i2c_bit_bang.cpp @@ -0,0 +1,261 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace hal { +// public +i2c_bit_bang::i2c_bit_bang(output_pin& p_scl, + output_pin& p_sda, + steady_clock& p_clock, + bus_info p_bus) + : m_scl(&p_scl) + , m_sda(&p_sda) + , m_clock(&p_clock) + , m_bus(p_bus) +{ +} + +void i2c_bit_bang::driver_configure(const settings& p_settings) +{ + using namespace std::chrono_literals; + + if (p_settings.clock_rate > m_clock->frequency()) { + throw hal::operation_not_supported(this); + } + + // calculate period in nanosecond + float nanoseconds_per_second = 1'000'000'000.0f; + uint32_t period_ns = nanoseconds_per_second / p_settings.clock_rate; + auto scl_high_time = static_cast( + static_cast(period_ns * m_bus.duty_cycle)); + auto scl_low_time = + static_cast(period_ns) - scl_high_time; + + // calculate ticks for high and low + using period = decltype(scl_high_time)::period; + const auto frequency = m_clock->frequency(); + const auto tick_period = hal::wavelength(frequency); + + auto ticks_required_high = scl_high_time / tick_period; + using unsigned_ticks = std::make_unsigned_t; + m_scl_high_ticks = static_cast(ticks_required_high); + + auto ticks_required_low = scl_low_time / tick_period; + m_scl_low_ticks = static_cast(ticks_required_low); + + // calculate the delay due to the uptime function call + const auto callibration_start_tick = m_clock->uptime(); + const auto callibration_end_tick = m_clock->uptime(); + m_uptime_ticks = callibration_end_tick - callibration_start_tick; + + // calculating output_pin going to true delay time + const auto before_output_high = m_clock->uptime(); + m_scl->level(true); + const auto after_output_high = m_clock->uptime(); + const auto output_true_ticks = after_output_high - before_output_high; + + m_scl_high_ticks -= output_true_ticks; + + // calculating output_pin going to false delay time + const auto before_output_low = m_clock->uptime(); + m_scl->level(false); + const auto after_output_low = m_clock->uptime(); + const auto output_false_ticks = after_output_low - before_output_low; + + m_scl_low_ticks -= output_false_ticks; +} + +void i2c_bit_bang::driver_transaction( + hal::byte p_address, + std::span p_data_out, + std::span p_data_in, + function_ref p_timeout) +{ + + hal::i2c_operation operation; + hal::byte address_to_write; + + // select read or write + if (!p_data_out.empty()) { + send_start_condition(); + operation = hal::i2c_operation::write; + address_to_write = hal::to_8_bit_address(p_address, operation); + + write_address(address_to_write, p_timeout); + + write(p_data_out, p_timeout); + } + + // if the first bit is 1 then we read + if (!p_data_in.empty()) { + send_start_condition(); + + operation = hal::i2c_operation::read; + address_to_write = hal::to_8_bit_address(p_address, operation); + + write_address(address_to_write, p_timeout); + + read(p_data_in, p_timeout); + } + + send_stop_condition(); +} + +// private + +void i2c_bit_bang::send_start_condition() +{ + using namespace std::chrono_literals; + // the start condition requires both the sda and scl lines to be pulled high + // before sending, so we do that here. + m_sda->level(true); + m_scl->level(true); + high_speed_delay(m_scl_high_ticks); + m_sda->level(false); + high_speed_delay(m_scl_high_ticks); + m_scl->level(false); + high_speed_delay(m_scl_high_ticks); +} + +void i2c_bit_bang::send_stop_condition() +{ + m_sda->level(false); + + m_scl->level(true); + high_speed_delay(m_scl_high_ticks); + m_sda->level(true); + high_speed_delay(m_scl_high_ticks); +} + +void i2c_bit_bang::write_address(hal::byte p_address, + function_ref p_timeout) +{ + // write the address + auto acknowledged = write_byte(p_address, p_timeout); + + if (!acknowledged) { + hal::safe_throw(hal::no_such_device((p_address >> 1), this)); + } +} + +void i2c_bit_bang::write(std::span p_data_out, + function_ref p_timeout) +{ + bool acknowledged; + for (const hal::byte& data : p_data_out) { + + acknowledged = write_byte(data, p_timeout); + + if (!acknowledged) { + hal::safe_throw(hal::io_error(this)); + } + } +} + +bool i2c_bit_bang::write_byte(hal::byte p_byte_to_write, + function_ref p_timeout) +{ + using namespace std::chrono_literals; + constexpr auto byte_length = 8; + constexpr hal::byte bit_select = 0b0000'0001; + hal::byte bit_to_write = 0; + uint32_t i; + for (i = 0; i < byte_length; i++) { + + bit_to_write = static_cast( + (p_byte_to_write >> (byte_length - (i + 1))) & bit_select); + + write_bit(bit_to_write, p_timeout); + } + + // look for the ack + auto ack_bit = read_bit(); + // if ack bit is 0, then it was acknowledged (true) + return (ack_bit == 0); +} +/* + for writing a bit you want to make set the data line first, then toggle the + level of the clock then check if the level was indeed toggled or if the + peripheral is stretching the clock. After this is done, you are able to set + the clock back low. +*/ +void i2c_bit_bang::write_bit(hal::byte p_bit_to_write, + function_ref p_timeout) +{ + m_sda->level(static_cast(p_bit_to_write)); + m_scl->level(true); + high_speed_delay(m_scl_high_ticks); + + // if scl is still low after we set it high, then the peripheral is clock + // stretching + while (m_scl->level() == 0) { + p_timeout(); + } + + m_scl->level(false); + high_speed_delay(m_scl_low_ticks); +} + +void i2c_bit_bang::read(std::span p_data_in, + function_ref p_timeout) +{ + uint32_t size_of_span = p_data_in.size(), i = 0; + for (hal::byte& data : p_data_in) { + data = read_byte(); + i++; + + if (i < size_of_span) { + // if the iterator isn't done, then we ack whatever data we read + write_bit(0, p_timeout); + + } else { + // when the data is done being read in, then send a NACK to tell the + // slave to stop reading + write_bit(1, p_timeout); + } + } +} + +hal::byte i2c_bit_bang::read_byte() +{ + constexpr auto byte_length = 8; + hal::byte read_byte = 0; + uint32_t i; + for (i = 1; i <= byte_length; i++) { + read_byte |= (read_bit() << (byte_length - i)); + } + return read_byte; +} + +hal::byte i2c_bit_bang::read_bit() +{ + m_sda->level(true); + m_scl->level(true); + high_speed_delay(m_scl_high_ticks); + + auto bit_read = static_cast(m_sda->level()); + + m_scl->level(false); + high_speed_delay(m_scl_low_ticks); + + return bit_read; +} + +void i2c_bit_bang::high_speed_delay(uint64_t ticks) { + const auto start_time_high = m_clock->uptime(); + uint64_t uptime = 0; + + const auto ticks_until_timeout_high = ticks + start_time_high; + + while (uptime < ticks_until_timeout_high) { + uptime = m_clock->uptime() + m_uptime_ticks; + continue; + } +} + +} // namespace hal