blob: b69e533c265f79671da2504de031c135dea6e53d [file] [log] [blame]
// Copyright 2020 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 <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/device-protocol/i2c-channel.h>
#include <lib/fake-i2c/fake-i2c.h>
#include <zircon/errors.h>
#include <algorithm>
#include <numeric>
#include <vector>
#include <zxtest/zxtest.h>
#include "src/devices/testing/mock-ddk/mock-device.h"
// An I2C device that requires retries.
class FlakyI2cDevice : public fake_i2c::FakeI2c {
protected:
zx_status_t Transact(const uint8_t* write_buffer, size_t write_buffer_size, uint8_t* read_buffer,
size_t* read_buffer_size) override {
count_++;
// Unique errors below to check for retries.
switch (count_) {
// clang-format off
case 1: return ZX_ERR_INTERNAL; break;
case 2: return ZX_ERR_NOT_SUPPORTED; break;
case 3: return ZX_ERR_NO_RESOURCES; break;
case 4: return ZX_ERR_NO_MEMORY; break;
case 5: *read_buffer_size = 1; return ZX_OK; break;
default: ZX_ASSERT(0); // Anything else is an error.
// clang-format on
}
return ZX_OK;
}
private:
size_t count_ = 0;
};
class I2cDevice : public fake_i2c::FakeI2c {
public:
size_t banjo_count() const { return banjo_count_; }
size_t fidl_count() const { return fidl_count_; }
void Transfer(TransferRequestView request, TransferCompleter::Sync& completer) override {
fidl_count_++;
const fidl::VectorView<fuchsia_hardware_i2c::wire::Transaction> transactions =
request->transactions;
if (std::any_of(transactions.cbegin(), transactions.cend(),
[](const fuchsia_hardware_i2c::wire::Transaction& t) {
return !t.has_data_transfer();
})) {
completer.ReplyError(ZX_ERR_INVALID_ARGS);
return;
}
fidl::Arena arena;
fidl::ObjectView<fuchsia_hardware_i2c::wire::DeviceTransferResponse> response(arena);
stop_.reserve(transactions.count());
const size_t write_size = std::accumulate(
transactions.cbegin(), transactions.cend(), 0,
[](size_t a, const fuchsia_hardware_i2c::wire::Transaction& b) {
return a +
(b.data_transfer().is_write_data() ? b.data_transfer().write_data().count() : 0);
});
tx_data_.resize(write_size);
const size_t read_count = std::count_if(transactions.cbegin(), transactions.cend(),
[](const fuchsia_hardware_i2c::wire::Transaction& t) {
return t.data_transfer().is_read_size();
});
response->read_data = {arena, read_count};
size_t tx_offset = 0;
size_t rx_transaction = 0;
size_t rx_offset = 0;
auto stop_it = stop_.begin();
for (const auto& transaction : transactions) {
if (transaction.data_transfer().is_read_size()) {
// If this is a write/read, pass back all of the expected read data, regardless of how much
// the client requested. This allows the truncation behavior to be tested.
const size_t read_size =
read_count == 1 ? rx_data_.size() : transaction.data_transfer().read_size();
// Copy the expected RX data to each read transaction.
auto& read_data = response->read_data[rx_transaction++];
read_data = {arena, read_size};
if (rx_offset + read_data.count() > rx_data_.size()) {
completer.ReplyError(ZX_ERR_OUT_OF_RANGE);
return;
}
memcpy(read_data.mutable_data(), &rx_data_[rx_offset], read_data.count());
rx_offset += transaction.data_transfer().read_size();
} else {
// Serialize and store the write transaction.
const auto& write_data = transaction.data_transfer().write_data();
memcpy(&tx_data_[tx_offset], write_data.data(), write_data.count());
tx_offset += write_data.count();
}
*stop_it++ = transaction.has_stop() ? transaction.stop() : false;
}
completer.Reply(::fitx::ok(response.get()));
}
const std::vector<uint8_t>& tx_data() const { return tx_data_; }
void set_rx_data(std::vector<uint8_t> rx_data) { rx_data_ = std::move(rx_data); }
const std::vector<bool>& stop() const { return stop_; }
protected:
zx_status_t Transact(const uint8_t* write_buffer, size_t write_buffer_size, uint8_t* read_buffer,
size_t* read_buffer_size) override {
banjo_count_++;
*read_buffer_size = rx_data_.size();
memcpy(read_buffer, rx_data_.data(), rx_data_.size());
tx_data_ = {write_buffer, write_buffer + write_buffer_size};
return ZX_OK;
}
private:
size_t banjo_count_ = 0;
size_t fidl_count_ = 0;
std::vector<uint8_t> tx_data_;
std::vector<uint8_t> rx_data_;
std::vector<bool> stop_;
};
class I2cChannelTest : public zxtest::Test {
public:
I2cChannelTest() : loop_(&kAsyncLoopConfigNoAttachToCurrentThread) {}
void SetUp() override { loop_.StartThread(); }
void TearDown() override { loop_.Shutdown(); }
protected:
static void TransactCallback(void* ctx, zx_status_t status, const i2c_op_t* op_list,
size_t op_count) {
reinterpret_cast<I2cChannelTest*>(ctx)->TransactCallback(status, op_list, op_count);
}
void TransactCallback(zx_status_t status, const i2c_op_t* op_list, size_t op_count) {
ASSERT_LE(op_count, I2C_MAX_RW_OPS);
read_ops_ = op_count;
for (size_t i = 0; i < op_count; i++) {
read_data_[i] = {op_list[i].data_buffer, op_list[i].data_buffer + op_list[i].data_size};
}
transact_status_ = status;
}
fidl::ClientEnd<fuchsia_hardware_i2c::Device> BindI2c(
fidl::WireServer<fuchsia_hardware_i2c::Device>* server) {
auto endpoints = fidl::CreateEndpoints<fuchsia_hardware_i2c::Device>();
EXPECT_TRUE(endpoints.is_ok());
fidl::BindServer(loop_.dispatcher(), std::move(endpoints->server), server);
return std::move(endpoints->client);
}
std::vector<uint8_t> read_data_[I2C_MAX_RW_OPS];
size_t read_ops_ = 0;
zx_status_t transact_status_ = ZX_ERR_IO;
async::Loop loop_;
};
TEST_F(I2cChannelTest, NoRetries) {
FlakyI2cDevice i2c_dev;
ddk::I2cChannel channel(BindI2c(&i2c_dev));
// No retry, the first error is returned.
uint8_t buffer[1] = {0x12};
constexpr uint8_t kNumberOfRetries = 0;
auto ret = channel.WriteSyncRetries(buffer, sizeof(buffer), kNumberOfRetries, zx::usec(1));
EXPECT_EQ(ret.status, ZX_ERR_INTERNAL);
EXPECT_EQ(ret.retries, 0);
}
TEST_F(I2cChannelTest, RetriesAllFail) {
FlakyI2cDevice i2c_dev;
ddk::I2cChannel channel(BindI2c(&i2c_dev));
// 2 retries, corresponding error is returned. The first time Transact is called we get a
// ZX_ERR_INTERNAL. Then the first retry gives us ZX_ERR_NOT_SUPPORTED and then the second
// gives us ZX_ERR_NO_RESOURCES.
constexpr uint8_t kNumberOfRetries = 2;
uint8_t buffer[1] = {0x34};
auto ret = channel.ReadSyncRetries(0x56, buffer, sizeof(buffer), kNumberOfRetries, zx::usec(1));
EXPECT_EQ(ret.status, ZX_ERR_NO_RESOURCES);
EXPECT_EQ(ret.retries, 2);
}
TEST_F(I2cChannelTest, RetriesOk) {
FlakyI2cDevice i2c_dev;
ddk::I2cChannel channel(BindI2c(&i2c_dev));
// 4 retries requested but no error, return ok.
uint8_t tx_buffer[1] = {0x78};
uint8_t rx_buffer[1] = {0x90};
constexpr uint8_t kNumberOfRetries = 5;
auto ret = channel.WriteReadSyncRetries(tx_buffer, sizeof(tx_buffer), rx_buffer,
sizeof(rx_buffer), kNumberOfRetries, zx::usec(1));
EXPECT_EQ(ret.status, ZX_OK);
EXPECT_EQ(ret.retries, 4);
}
TEST_F(I2cChannelTest, FidlRead) {
auto endpoints = fidl::CreateEndpoints<fuchsia_hardware_i2c::Device>();
ASSERT_TRUE(endpoints.is_ok());
I2cDevice i2c_dev;
auto binding = fidl::BindServer(loop_.dispatcher(), std::move(endpoints->server), &i2c_dev);
const std::array<uint8_t, 4> expected_rx_data{0x12, 0x34, 0xab, 0xcd};
ddk::I2cFidlChannel client(std::move(endpoints->client));
i2c_dev.set_rx_data({expected_rx_data.data(), expected_rx_data.data() + expected_rx_data.size()});
uint8_t buf[4];
EXPECT_OK(client.ReadSync(0x89, buf, expected_rx_data.size()));
ASSERT_EQ(i2c_dev.tx_data().size(), 1);
EXPECT_EQ(i2c_dev.tx_data()[0], 0x89);
EXPECT_BYTES_EQ(buf, expected_rx_data.data(), expected_rx_data.size());
uint8_t buf1[5];
// I2cChannel will copy as much data as it receives, even if it is less than the amount requested.
EXPECT_OK(client.ReadSync(0x98, buf1, sizeof(buf1)));
ASSERT_EQ(i2c_dev.tx_data().size(), 1);
EXPECT_EQ(i2c_dev.tx_data()[0], 0x98);
EXPECT_BYTES_EQ(buf1, expected_rx_data.data(), expected_rx_data.size());
uint8_t buf2[3];
// I2cChannel will copy no more than the amount requested.
EXPECT_OK(client.ReadSync(0x18, buf2, sizeof(buf2)));
ASSERT_EQ(i2c_dev.tx_data().size(), 1);
EXPECT_EQ(i2c_dev.tx_data()[0], 0x18);
EXPECT_BYTES_EQ(buf2, expected_rx_data.data(), sizeof(buf2));
}
TEST_F(I2cChannelTest, FidlWrite) {
auto endpoints = fidl::CreateEndpoints<fuchsia_hardware_i2c::Device>();
ASSERT_TRUE(endpoints.is_ok());
I2cDevice i2c_dev;
auto binding = fidl::BindServer(loop_.dispatcher(), std::move(endpoints->server), &i2c_dev);
const std::array<uint8_t, 4> expected_tx_data{0x0f, 0x1e, 0x2d, 0x3c};
ddk::I2cFidlChannel client(std::move(endpoints->client));
EXPECT_OK(client.WriteSync(expected_tx_data.data(), expected_tx_data.size()));
ASSERT_EQ(i2c_dev.tx_data().size(), expected_tx_data.size());
EXPECT_BYTES_EQ(i2c_dev.tx_data().data(), expected_tx_data.data(), expected_tx_data.size());
}
TEST_F(I2cChannelTest, FidlWriteRead) {
auto endpoints = fidl::CreateEndpoints<fuchsia_hardware_i2c::Device>();
ASSERT_TRUE(endpoints.is_ok());
I2cDevice i2c_dev;
auto binding = fidl::BindServer(loop_.dispatcher(), std::move(endpoints->server), &i2c_dev);
const std::array<uint8_t, 4> expected_rx_data{0x12, 0x34, 0xab, 0xcd};
const std::array<uint8_t, 4> expected_tx_data{0x0f, 0x1e, 0x2d, 0x3c};
ddk::I2cFidlChannel client(std::move(endpoints->client));
i2c_dev.set_rx_data({expected_rx_data.data(), expected_rx_data.data() + expected_rx_data.size()});
uint8_t buf[4];
EXPECT_OK(client.WriteReadSync(expected_tx_data.data(), expected_tx_data.size(), buf,
expected_rx_data.size()));
ASSERT_EQ(i2c_dev.tx_data().size(), expected_tx_data.size());
EXPECT_BYTES_EQ(i2c_dev.tx_data().data(), expected_tx_data.data(), expected_tx_data.size());
EXPECT_BYTES_EQ(buf, expected_rx_data.data(), expected_rx_data.size());
uint8_t buf1[5];
EXPECT_OK(
client.WriteReadSync(expected_tx_data.data(), expected_tx_data.size(), buf1, sizeof(buf1)));
EXPECT_BYTES_EQ(i2c_dev.tx_data().data(), expected_tx_data.data(), expected_tx_data.size());
EXPECT_BYTES_EQ(buf1, expected_rx_data.data(), expected_rx_data.size());
uint8_t buf2[3];
EXPECT_OK(
client.WriteReadSync(expected_tx_data.data(), expected_tx_data.size(), buf2, sizeof(buf2)));
EXPECT_BYTES_EQ(i2c_dev.tx_data().data(), expected_tx_data.data(), expected_tx_data.size());
EXPECT_BYTES_EQ(buf2, expected_rx_data.data(), sizeof(buf2));
EXPECT_OK(client.WriteReadSync(nullptr, 0, buf, expected_rx_data.size()));
EXPECT_EQ(i2c_dev.tx_data().size(), 0);
EXPECT_BYTES_EQ(buf, expected_rx_data.data(), expected_rx_data.size());
}
TEST_F(I2cChannelTest, FidlTransfer) {
auto endpoints = fidl::CreateEndpoints<fuchsia_hardware_i2c::Device>();
ASSERT_TRUE(endpoints.is_ok());
I2cDevice i2c_dev;
auto binding = fidl::BindServer(loop_.dispatcher(), std::move(endpoints->server), &i2c_dev);
auto parent = MockDevice::FakeRootParent();
parent->AddFidlProtocol(fidl::DiscoverableProtocolName<fuchsia_hardware_i2c::Device>,
[this, &i2c_dev](zx::channel channel) {
fidl::BindServer(
loop_.dispatcher(),
fidl::ServerEnd<fuchsia_hardware_i2c::Device>(std::move(channel)),
&i2c_dev);
return ZX_OK;
});
ddk::I2cChannel client(parent.get());
ASSERT_TRUE(client.is_valid());
const std::array<uint8_t, 4> expected_rx_data{0x12, 0x34, 0xab, 0xcd};
const std::array<uint8_t, 4> expected_tx_data{0x0f, 0x1e, 0x2d, 0x3c};
i2c_dev.set_rx_data({expected_rx_data.data(), expected_rx_data.data() + expected_rx_data.size()});
i2c_op_t ops[4];
ops[0] = {
.data_buffer = expected_tx_data.data(),
.data_size = 2,
.is_read = false,
.stop = true,
};
ops[1] = {
.data_buffer = nullptr,
.data_size = 2,
.is_read = true,
.stop = false,
};
ops[2] = {
.data_buffer = expected_tx_data.data() + 2,
.data_size = 2,
.is_read = false,
.stop = true,
};
ops[3] = {
.data_buffer = nullptr,
.data_size = 2,
.is_read = true,
.stop = false,
};
client.Transact(ops, std::size(ops), TransactCallback, this);
EXPECT_OK(transact_status_);
ASSERT_EQ(read_ops_, 2);
ASSERT_EQ(read_data_[0].size(), 2);
EXPECT_BYTES_EQ(read_data_[0].data(), expected_rx_data.data(), 2);
ASSERT_EQ(read_data_[1].size(), 2);
EXPECT_BYTES_EQ(read_data_[1].data(), expected_rx_data.data() + 2, 2);
ASSERT_EQ(i2c_dev.tx_data().size(), expected_tx_data.size());
EXPECT_BYTES_EQ(i2c_dev.tx_data().data(), expected_tx_data.data(), expected_tx_data.size());
EXPECT_TRUE(i2c_dev.stop()[0]);
EXPECT_FALSE(i2c_dev.stop()[1]);
EXPECT_TRUE(i2c_dev.stop()[2]);
EXPECT_FALSE(i2c_dev.stop()[3]);
}
TEST_F(I2cChannelTest, GetFidlProtocolFromParent) {
auto endpoints = fidl::CreateEndpoints<fuchsia_hardware_i2c::Device>();
ASSERT_TRUE(endpoints.is_ok());
I2cDevice i2c_dev;
auto binding = fidl::BindServer(loop_.dispatcher(), std::move(endpoints->server), &i2c_dev);
auto parent = MockDevice::FakeRootParent();
parent->AddFidlProtocol(fidl::DiscoverableProtocolName<fuchsia_hardware_i2c::Device>,
[this, &i2c_dev](zx::channel channel) {
fidl::BindServer(
loop_.dispatcher(),
fidl::ServerEnd<fuchsia_hardware_i2c::Device>(std::move(channel)),
&i2c_dev);
return ZX_OK;
});
ddk::I2cChannel client(parent.get());
ASSERT_TRUE(client.is_valid());
// Issue a simple call to make sure the connection is working.
i2c_dev.set_rx_data({0xab});
uint8_t rx;
EXPECT_OK(client.ReadSync(0x89, &rx, 1));
ASSERT_EQ(i2c_dev.tx_data().size(), 1);
EXPECT_EQ(i2c_dev.tx_data()[0], 0x89);
EXPECT_EQ(rx, 0xab);
EXPECT_EQ(i2c_dev.banjo_count(), 0);
EXPECT_EQ(i2c_dev.fidl_count(), 1);
// Move the client and verify that the new one is functional.
ddk::I2cChannel new_client = std::move(client);
i2c_dev.set_rx_data({0x12});
EXPECT_OK(new_client.ReadSync(0x34, &rx, 1));
ASSERT_EQ(i2c_dev.tx_data().size(), 1);
EXPECT_EQ(i2c_dev.tx_data()[0], 0x34);
EXPECT_EQ(rx, 0x12);
EXPECT_EQ(i2c_dev.banjo_count(), 0);
EXPECT_EQ(i2c_dev.fidl_count(), 2);
}
TEST_F(I2cChannelTest, GetFidlProtocolFromFragment) {
auto endpoints = fidl::CreateEndpoints<fuchsia_hardware_i2c::Device>();
ASSERT_TRUE(endpoints.is_ok());
I2cDevice i2c_dev;
auto binding = fidl::BindServer(loop_.dispatcher(), std::move(endpoints->server), &i2c_dev);
auto parent = MockDevice::FakeRootParent();
parent->AddFidlProtocol(
fidl::DiscoverableProtocolName<fuchsia_hardware_i2c::Device>,
[this, &i2c_dev](zx::channel channel) {
fidl::BindServer(loop_.dispatcher(),
fidl::ServerEnd<fuchsia_hardware_i2c::Device>(std::move(channel)),
&i2c_dev);
return ZX_OK;
},
"fragment-name");
ddk::I2cChannel client(parent.get(), "fragment-name");
ASSERT_TRUE(client.is_valid());
i2c_dev.set_rx_data({0x56});
uint8_t rx;
EXPECT_OK(client.ReadSync(0x78, &rx, 1));
ASSERT_EQ(i2c_dev.tx_data().size(), 1);
EXPECT_EQ(i2c_dev.tx_data()[0], 0x78);
EXPECT_EQ(rx, 0x56);
EXPECT_EQ(i2c_dev.banjo_count(), 0);
EXPECT_EQ(i2c_dev.fidl_count(), 1);
}
TEST_F(I2cChannelTest, BanjoClientMethods) {
auto endpoints = fidl::CreateEndpoints<fuchsia_hardware_i2c::Device>();
ASSERT_TRUE(endpoints.is_ok());
I2cDevice i2c_dev;
auto binding = fidl::BindServer(loop_.dispatcher(), std::move(endpoints->server), &i2c_dev);
auto parent = MockDevice::FakeRootParent();
parent->AddFidlProtocol(fidl::DiscoverableProtocolName<fuchsia_hardware_i2c::Device>,
[this, &i2c_dev](zx::channel channel) {
fidl::BindServer(
loop_.dispatcher(),
fidl::ServerEnd<fuchsia_hardware_i2c::Device>(std::move(channel)),
&i2c_dev);
return ZX_OK;
});
ddk::I2cChannel client(parent.get());
ASSERT_TRUE(client.is_valid());
const std::array<uint8_t, 4> expected_rx_data{0x12, 0x34, 0xab, 0xcd};
const std::array<uint8_t, 1> expected_tx_data{0xa5};
i2c_op_t ops[2] = {
{
.data_buffer = expected_tx_data.data(),
.data_size = expected_tx_data.size(),
.is_read = false,
.stop = false,
},
{
.data_buffer = nullptr,
.data_size = expected_rx_data.size(),
.is_read = true,
.stop = true,
},
};
i2c_dev.set_rx_data({expected_rx_data.data(), expected_rx_data.data() + expected_rx_data.size()});
struct I2cContext {
sync_completion_t completion = {};
zx_status_t status = ZX_ERR_INTERNAL;
uint8_t rx_data[4];
} context;
client.Transact(
ops, std::size(ops),
[](void* ctx, zx_status_t status, const i2c_op_t* op_list, size_t op_count) {
auto* i2c_context = reinterpret_cast<I2cContext*>(ctx);
ASSERT_EQ(op_count, 1);
ASSERT_TRUE(op_list[0].is_read);
ASSERT_EQ(op_list[0].data_size, 4);
memcpy(i2c_context->rx_data, op_list[0].data_buffer, 4);
i2c_context->status = status;
sync_completion_signal(&i2c_context->completion);
},
&context);
ASSERT_OK(sync_completion_wait(&context.completion, ZX_TIME_INFINITE));
EXPECT_OK(context.status);
ASSERT_EQ(i2c_dev.tx_data().size(), 1);
EXPECT_EQ(i2c_dev.tx_data()[0], 0xa5);
EXPECT_BYTES_EQ(context.rx_data, expected_rx_data.data(), sizeof(context.rx_data));
}