| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "i2c-bus.h" |
| |
| #include <lib/device-protocol/i2c.h> |
| #include <lib/zx/profile.h> |
| #include <lib/zx/thread.h> |
| #include <lib/zx/time.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <threads.h> |
| #include <zircon/assert.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/threads.h> |
| |
| #include <mutex> |
| |
| #include <ddk/debug.h> |
| #include <ddk/trace/event.h> |
| #include <fbl/alloc_checker.h> |
| #include <fbl/array.h> |
| #include <fbl/auto_lock.h> |
| |
| namespace i2c { |
| |
| I2cBus::I2cBus(zx_device_t* parent, ddk::I2cImplProtocolClient i2c, uint32_t bus_id) |
| : parent_(parent), i2c_(i2c), bus_id_(bus_id) { |
| list_initialize(&queued_txns_); |
| list_initialize(&free_txns_); |
| sync_completion_reset(&txn_signal_); |
| } |
| |
| zx_status_t I2cBus::Start() { |
| auto status = i2c_.GetMaxTransferSize(bus_id_, &max_transfer_); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| char name[32]; |
| snprintf(name, sizeof(name), "I2cBus[%u]", bus_id_); |
| auto thunk = [](void* arg) -> int { return static_cast<I2cBus*>(arg)->I2cThread(); }; |
| thrd_create_with_name(&thread_, thunk, this, name); |
| |
| // Set profile for bus transaction thread. |
| // TODO(fxbug.dev/40858): Migrate to the role-based API when available, instead of hard |
| // coding parameters. |
| { |
| const zx::duration capacity = zx::usec(100); |
| const zx::duration deadline = zx::msec(1); |
| const zx::duration period = deadline; |
| |
| zx::profile bus_profile; |
| status = device_get_deadline_profile(parent_, capacity.get(), deadline.get(), period.get(), |
| name, bus_profile.reset_and_get_address()); |
| if (status != ZX_OK) { |
| zxlogf(WARNING, "I2cBus::Start: Failed to get deadline profile: %s", |
| zx_status_get_string(status)); |
| } else { |
| status = zx_object_set_profile(thrd_get_zx_handle(thread_), bus_profile.get(), 0); |
| if (status != ZX_OK) { |
| zxlogf(WARNING, "I2cBus::Start: Failed to apply deadline profile to bus thread: %s", |
| zx_status_get_string(status)); |
| } |
| } |
| } |
| |
| return ZX_OK; |
| } |
| |
| int I2cBus::I2cThread() { |
| fbl::AllocChecker ac; |
| fbl::Array<uint8_t> read_buffer(new (&ac) uint8_t[I2C_MAX_TOTAL_TRANSFER], |
| I2C_MAX_TOTAL_TRANSFER); |
| if (!ac.check()) { |
| zxlogf(ERROR, "%s could not allocate read_buffer", __FUNCTION__); |
| return 0; |
| } |
| |
| while (1) { |
| sync_completion_wait(&txn_signal_, ZX_TIME_INFINITE); |
| sync_completion_reset(&txn_signal_); |
| I2cTxn* txn; |
| |
| mutex_.Acquire(); |
| while ((txn = list_remove_head_type(&queued_txns_, I2cTxn, node)) != nullptr) { |
| mutex_.Release(); |
| |
| TRACE_DURATION("i2c", "I2cBus Process Queued Transacts"); |
| TRACE_FLOW_END("i2c", "I2cBus Transact Flow", txn->trace_id, "Flow", txn->trace_id); |
| |
| auto op_list = reinterpret_cast<i2c_op_t*>(txn + 1); |
| auto op_count = txn->op_count; |
| auto p_writes = reinterpret_cast<uint8_t*>(op_list) + op_count * sizeof(i2c_op_t); |
| uint8_t* p_reads = read_buffer.data(); |
| |
| ZX_ASSERT(op_count < I2C_MAX_RW_OPS); |
| i2c_impl_op_t impl_ops[I2C_MAX_RW_OPS]; |
| for (size_t i = 0; i < op_count; ++i) { |
| // Same address for all ops, since there is one address per channel. |
| impl_ops[i].address = txn->address; |
| impl_ops[i].data_size = op_list[i].data_size; |
| impl_ops[i].is_read = op_list[i].is_read; |
| impl_ops[i].stop = op_list[i].stop; |
| if (impl_ops[i].is_read) { |
| impl_ops[i].data_buffer = p_reads; |
| p_reads += impl_ops[i].data_size; |
| } else { |
| impl_ops[i].data_buffer = p_writes; |
| p_writes += impl_ops[i].data_size; |
| } |
| } |
| auto status = i2c_.Transact(bus_id_, impl_ops, op_count); |
| |
| if (status == ZX_OK) { |
| i2c_op_t read_ops[I2C_MAX_RW_OPS]; |
| size_t read_ops_cnt = 0; |
| for (size_t i = 0; i < op_count; ++i) { |
| if (op_list[i].is_read) { |
| read_ops[read_ops_cnt] = op_list[i]; |
| read_ops[read_ops_cnt].data_buffer = impl_ops[i].data_buffer; |
| read_ops_cnt++; |
| } |
| } |
| txn->transact_cb(txn->cookie, ZX_OK, read_ops, read_ops_cnt); |
| } else { |
| txn->transact_cb(txn->cookie, status, nullptr, 0); |
| } |
| |
| mutex_.Acquire(); |
| list_add_tail(&free_txns_, &txn->node); |
| } |
| mutex_.Release(); |
| } |
| return 0; |
| } |
| |
| void I2cBus::Transact(uint16_t address, const i2c_op_t* op_list, size_t op_count, |
| i2c_transact_callback callback, void* cookie) { |
| TRACE_DURATION("i2c", "I2cBus Queue Transact"); |
| |
| // TODO(fxbug.dev/52177): This is a hack to apply a deadline profile to the dispatch |
| // thread for this devhost. Replace this with a proper solution. |
| static std::once_flag profile_flag; |
| std::call_once(profile_flag, [device = parent_] { |
| // Set profile for bus transaction thread. |
| // TODO(fxbug.dev/40858): Migrate to the role-based API when available, instead of hard |
| // coding parameters. |
| const zx::duration capacity = zx::usec(150); |
| const zx::duration deadline = zx::msec(1); |
| const zx::duration period = deadline; |
| |
| zx::profile profile; |
| zx_status_t status = |
| device_get_deadline_profile(device, capacity.get(), deadline.get(), period.get(), |
| "I2cBus Dispatcher", profile.reset_and_get_address()); |
| if (status != ZX_OK) { |
| zxlogf(WARNING, "I2cBus::Transact: Failed to get deadline profile: %s", |
| zx_status_get_string(status)); |
| } else { |
| status = zx::thread::self()->set_profile(profile, 0); |
| if (status != ZX_OK) { |
| zxlogf(WARNING, "I2cBus::Transact: Failed to apply deadline profile to dispatch thread: %s", |
| zx_status_get_string(status)); |
| } |
| } |
| }); |
| |
| size_t writes_length = 0; |
| for (size_t i = 0; i < op_count; ++i) { |
| if (op_list[i].data_size == 0 || op_list[i].data_size > max_transfer_) { |
| callback(cookie, ZX_ERR_INVALID_ARGS, nullptr, 0); |
| return; |
| } |
| if (!op_list[i].is_read) { |
| writes_length += op_list[i].data_size; |
| } |
| } |
| // Add space for requests and writes data. |
| size_t req_length = sizeof(I2cTxn) + op_count * sizeof(i2c_op_t) + writes_length; |
| if (req_length >= I2C_MAX_TOTAL_TRANSFER) { |
| callback(cookie, ZX_ERR_BUFFER_TOO_SMALL, nullptr, 0); |
| return; |
| } |
| |
| fbl::AutoLock lock(&mutex_); |
| |
| I2cTxn* txn = list_remove_head_type(&free_txns_, I2cTxn, node); |
| if (txn && txn->length < req_length) { |
| free(txn); |
| txn = nullptr; |
| } |
| |
| if (!txn) { |
| // add space for write buffer |
| txn = static_cast<I2cTxn*>(calloc(1, req_length)); |
| if (!txn) { |
| callback(cookie, ZX_ERR_NO_MEMORY, nullptr, 0); |
| return; |
| } |
| txn->length = req_length; |
| } |
| |
| txn->address = address; |
| txn->op_count = op_count; |
| txn->transact_cb = callback; |
| txn->cookie = cookie; |
| if (TRACE_ENABLED()) { |
| txn->trace_id = TRACE_NONCE(); |
| TRACE_FLOW_BEGIN("i2c", "I2cBus Transact Flow", txn->trace_id, "Flow", txn->trace_id); |
| } |
| |
| // copy the op_list |
| auto* dest = reinterpret_cast<uint8_t*>(txn + 1); |
| memcpy(dest, op_list, op_count * sizeof(op_list[0])); |
| dest += op_count * sizeof(op_list[0]); |
| // copy the write buffers |
| for (size_t i = 0; i < op_count; ++i) { |
| if (!op_list[i].is_read) { |
| memcpy(dest, op_list[i].data_buffer, op_list[i].data_size); |
| dest += op_list[i].data_size; |
| } |
| } |
| |
| list_add_tail(&queued_txns_, &txn->node); |
| sync_completion_signal(&txn_signal_); |
| } |
| |
| } // namespace i2c |