blob: 49662dd83c8f88357c8cd42d6b16b55fa5e319fb [file] [log] [blame]
// 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 "ftdi-i2c.h"
#include <fidl/fuchsia.hardware.ftdi/cpp/wire.h>
#include <inttypes.h>
#include <lib/ddk/debug.h>
#include <lib/ddk/device.h>
#include <lib/ddk/driver.h>
#include <stdlib.h>
#include <unistd.h>
#include <vector>
#include "ftdi.h"
namespace ftdi_mpsse {
zx_status_t FtdiI2c::Enable() {
zx_status_t status = mpsse_.Sync();
if (status != ZX_OK) {
zxlogf(ERROR, "ftdi_i2c: mpsse failed to sync %d", status);
return status;
}
status = mpsse_.FlushGpio();
if (status != ZX_OK) {
zxlogf(ERROR, "ftdi_i2c: mpsse failed flush GPIO");
return status;
}
status = mpsse_.SetClock(false, true, 100000);
if (status != ZX_OK) {
return status;
}
// Enable drive-zero mode -- this means sending 0 to GPIO drives outputs low
// and sending 1 drives them with tri-state. This matches the I2C protocol
// and lets multiple devices share the bus.
uint8_t buf[3] = {kFtdiCommandDriveZeroMode, 0x07, 0x00};
status = mpsse_.Write(buf, sizeof(buf));
if (status != ZX_OK) {
return status;
}
std::vector<uint8_t> buffer(6);
size_t bytes_written;
status = WriteIdleToBuf(0, &buffer, &bytes_written);
if (status != ZX_OK) {
return status;
}
status = mpsse_.Write(buffer.data(), buffer.size());
return ZX_OK;
}
zx_status_t FtdiI2c::Bind() {
{
fuchsia_hardware_i2cimpl::Service::InstanceHandler handler({
.device = bindings_.CreateHandler(this, fdf::Dispatcher::GetCurrent()->get(),
fidl::kIgnoreBindingClosure),
});
auto result = outgoing_.AddService<fuchsia_hardware_i2cimpl::Service>(std::move(handler));
if (result.is_error()) {
zxlogf(ERROR, "AddService failed: %s", result.status_string());
return result.error_value();
}
}
auto [directory_client, directory_server] = fidl::Endpoints<fuchsia_io::Directory>::Create();
{
auto result = outgoing_.Serve(std::move(directory_server));
if (result.is_error()) {
zxlogf(ERROR, "Failed to serve the outgoing directory: %s", result.status_string());
return result.error_value();
}
}
std::array fidl_service_offers = {
ddk::MetadataServer<fuchsia_hardware_i2c_businfo::I2CBusMetadata>::kFidlServiceName,
};
std::array<const char*, 1> runtime_service_offers{fuchsia_hardware_i2cimpl::Service::Name};
zx_status_t status = DdkAdd(ddk::DeviceAddArgs("ftdi-i2c")
.set_outgoing_dir(directory_client.TakeChannel())
.set_fidl_service_offers(fidl_service_offers)
.set_runtime_service_offers(runtime_service_offers));
if (status != ZX_OK) {
zxlogf(ERROR, "Failed to add device: %s", zx_status_get_string(status));
return status;
}
std::vector<fuchsia_hardware_i2c_businfo::I2CChannel> i2c_channels;
for (const auto& i2c_device : i2c_devices_) {
i2c_channels.emplace_back(fuchsia_hardware_i2c_businfo::I2CChannel{
{.address{static_cast<uint16_t>(i2c_device.address)},
.vid = i2c_device.vid,
.pid = i2c_device.pid,
.did = i2c_device.did}});
}
fuchsia_hardware_i2c_businfo::I2CBusMetadata metadata{{
.channels = std::move(i2c_channels),
.bus_id = 0,
}};
status = metadata_server_.SetMetadata(metadata);
if (status != ZX_OK) {
zxlogf(ERROR, "Failed to set metadata: %s", zx_status_get_string(status));
return status;
}
status = metadata_server_.Serve(outgoing_, fdf::Dispatcher::GetCurrent()->async_dispatcher());
if (status != ZX_OK) {
zxlogf(ERROR, "Failed to serve metadata: %s", zx_status_get_string(status));
return status;
}
return ZX_OK;
}
void FtdiI2c::DdkInit(ddk::InitTxn txn) {
// We will reply to the init txn once the device is ready to become visible
// and able to be unbound.
init_txn_ = std::move(txn);
auto f = [](void* arg) -> int {
auto dev = reinterpret_cast<FtdiI2c*>(arg);
dev->enable_thread_started_ = true;
zx_status_t status = dev->Enable();
dev->init_txn_->Reply(status); // Make the device visible and able to be unbound.
return status;
};
int rc = thrd_create_with_name(&enable_thread_, f, this, "ftdi-i2c-enable-thread");
if (rc != thrd_success) {
init_txn_->Reply(ZX_ERR_INTERNAL);
return;
}
// If the thread was created successfully, it will reply to the |init_txn_| once
// |Enable| completes, which will make the device visible and able to be unbound.
}
// This adds the command to set SCL and SDA high into buffer. It must be called
// at least once for initial setup.
zx_status_t FtdiI2c::WriteIdleToBuf(size_t index, std::vector<uint8_t>* buffer,
size_t* bytes_written) {
mpsse_.SetGpio(pin_layout_.scl, Mpsse::Direction::OUT, Mpsse::Level::HIGH);
mpsse_.SetGpio(pin_layout_.sda_out, Mpsse::Direction::OUT, Mpsse::Level::HIGH);
mpsse_.SetGpio(pin_layout_.sda_in, Mpsse::Direction::IN, Mpsse::Level::LOW);
mpsse_.GpioWriteCommandToBuffer(index, buffer, bytes_written);
return ZX_OK;
}
// This adds the command to write one byte over I2C into the buffer.
void FtdiI2c::WriteI2CByteWriteToBuf(size_t index, uint8_t byte, std::vector<uint8_t>* buffer,
size_t* bytes_written) {
size_t new_index = index;
(*buffer)[new_index++] = kI2cWriteCommandByte1;
(*buffer)[new_index++] = kI2cWriteCommandByte2;
(*buffer)[new_index++] = kI2cWriteCommandByte3;
(*buffer)[new_index++] = byte;
mpsse_.SetGpio(pin_layout_.scl, Mpsse::Direction::OUT, Mpsse::Level::LOW);
mpsse_.SetGpio(pin_layout_.sda_out, Mpsse::Direction::OUT, Mpsse::Level::HIGH);
size_t temp_written = 0;
mpsse_.GpioWriteCommandToBuffer(new_index, buffer, &temp_written);
new_index += temp_written;
// Read bit for ACK/NAK.
(*buffer)[new_index++] = kI2cReadAckCommandByte1;
(*buffer)[new_index++] = kI2cReadAckCommandByte2;
*bytes_written = new_index - index;
}
void FtdiI2c::WriteI2CByteReadToBuf(size_t index, bool final_byte, std::vector<uint8_t>* buffer,
size_t* bytes_written) {
size_t temp_written = 0;
size_t new_index = index;
if (final_byte) {
memcpy(buffer->data() + new_index, kI2cReadFinalByteCommand, sizeof(kI2cReadFinalByteCommand));
new_index += sizeof(kI2cReadFinalByteCommand);
} else {
memcpy(buffer->data() + new_index, kI2cReadOneByteCommand, sizeof(kI2cReadOneByteCommand));
new_index += sizeof(kI2cReadOneByteCommand);
}
mpsse_.SetGpio(pin_layout_.scl, Mpsse::Direction::OUT, Mpsse::Level::LOW);
mpsse_.SetGpio(pin_layout_.sda_out, Mpsse::Direction::OUT, Mpsse::Level::HIGH);
mpsse_.GpioWriteCommandToBuffer(new_index, buffer, &temp_written);
new_index += temp_written;
*bytes_written = new_index - index;
}
void FtdiI2c::DdkUnbind(ddk::UnbindTxn txn) {
if (enable_thread_started_) {
enable_thread_started_ = false;
thrd_join(enable_thread_, NULL);
}
txn.Reply();
}
zx_status_t FtdiI2c::Transact(uint8_t bus_address, std::vector<uint8_t> write_data,
std::vector<uint8_t>* read_data) {
size_t transaction_size;
size_t expected_reads = 0;
size_t read_size = (read_data == nullptr) ? 0 : read_data->size();
bool is_read = (read_size != 0);
if (!is_read) {
transaction_size =
kI2cNumCommandBytesPerFullWrite + (kI2cNumCommandBytesPerWriteByte * write_data.size());
} else {
transaction_size = kI2cNumCommandBytesPerFullReadWrite +
(kI2cNumCommandBytesPerWriteByte * write_data.size()) +
(kI2cNumCommandBytesPerReadByte * read_size);
}
std::vector<uint8_t> transaction(transaction_size);
size_t transaction_index = 0;
size_t bytes_written = 0;
zx_status_t status = WriteIdleToBuf(transaction_index, &transaction, &bytes_written);
if (status != ZX_OK) {
return status;
}
transaction_index += bytes_written;
status = WriteTransactionStartToBuf(transaction_index, &transaction, &bytes_written);
if (status != ZX_OK) {
return status;
}
transaction_index += bytes_written;
auto it = write_data.begin();
it = write_data.insert(it, static_cast<uint8_t>(bus_address << 1));
for (size_t i = 0; i < write_data.size(); i++) {
WriteI2CByteWriteToBuf(transaction_index, write_data[i], &transaction, &bytes_written);
transaction_index += bytes_written;
expected_reads++;
}
status = WriteTransactionEndToBuf(transaction_index, &transaction, &bytes_written);
if (status != ZX_OK) {
return status;
}
transaction_index += bytes_written;
if (is_read) {
zx_status_t status = WriteIdleToBuf(transaction_index, &transaction, &bytes_written);
if (status != ZX_OK) {
return status;
}
transaction_index += bytes_written;
status = WriteTransactionStartToBuf(transaction_index, &transaction, &bytes_written);
if (status != ZX_OK) {
return status;
}
transaction_index += bytes_written;
WriteI2CByteWriteToBuf(transaction_index, static_cast<uint8_t>(bus_address << 1 | 0x1),
&transaction, &bytes_written);
transaction_index += bytes_written;
expected_reads++;
// Send the read commands.
for (size_t i = 0; i < read_size; i++) {
WriteI2CByteReadToBuf(transaction_index, (i == (read_size - 1)), &transaction,
&bytes_written);
transaction_index += bytes_written;
expected_reads++;
}
status = WriteTransactionEndToBuf(transaction_index, &transaction, &bytes_written);
if (status != ZX_OK) {
return status;
}
transaction_index += bytes_written;
}
// Ask for response immediately
transaction[transaction_index++] = kI2cCommandFinishTransaction;
if (transaction_index != transaction.size()) {
return ZX_ERR_INTERNAL;
}
status = mpsse_.Write(transaction.data(), transaction.size());
if (status != ZX_OK) {
return status;
}
std::vector<uint8_t> response(expected_reads);
status = mpsse_.Read(response.data(), response.size());
if (status != ZX_OK) {
return status;
}
// Check each response byte to see if its an ACK (zero) or NACK (non-zero).
for (size_t i = 0; i < response.size() - read_size; i++) {
if ((response[i] & 0x1) != 0) {
zxlogf(INFO, "Ftdi-i2c: Received NACK on byte %ld (data=%d)", i, response[i]);
return ZX_ERR_INTERNAL;
}
}
// Copy the read information.
if (read_size) {
memcpy(read_data->data(), response.data() + (response.size() - read_size), read_size);
}
return ZX_OK;
}
zx_status_t FtdiI2c::Ping(uint8_t bus_address) {
std::vector<uint8_t> data(1);
data[0] = 0x00;
return Transact(bus_address, data, nullptr);
}
// This adds the command to transition SCL from high to low.
zx_status_t FtdiI2c::WriteTransactionStartToBuf(size_t index, std::vector<uint8_t>* buffer,
size_t* bytes_written) {
size_t sub_written = 0;
*bytes_written = 0;
mpsse_.SetGpio(pin_layout_.scl, Mpsse::Direction::OUT, Mpsse::Level::HIGH);
mpsse_.SetGpio(pin_layout_.sda_out, Mpsse::Direction::OUT, Mpsse::Level::LOW);
mpsse_.GpioWriteCommandToBuffer(index + *bytes_written, buffer, &sub_written);
*bytes_written += sub_written;
mpsse_.SetGpio(pin_layout_.scl, Mpsse::Direction::OUT, Mpsse::Level::LOW);
mpsse_.SetGpio(pin_layout_.sda_out, Mpsse::Direction::OUT, Mpsse::Level::LOW);
mpsse_.GpioWriteCommandToBuffer(index + *bytes_written, buffer, &sub_written);
*bytes_written += sub_written;
return ZX_OK;
}
zx_status_t FtdiI2c::WriteTransactionEndToBuf(size_t index, std::vector<uint8_t>* buffer,
size_t* bytes_written) {
size_t sub_written = 0;
*bytes_written = 0;
mpsse_.SetGpio(pin_layout_.scl, Mpsse::Direction::OUT, Mpsse::Level::LOW);
mpsse_.SetGpio(pin_layout_.sda_out, Mpsse::Direction::OUT, Mpsse::Level::LOW);
mpsse_.GpioWriteCommandToBuffer(index + *bytes_written, buffer, &sub_written);
*bytes_written += sub_written;
mpsse_.SetGpio(pin_layout_.scl, Mpsse::Direction::OUT, Mpsse::Level::HIGH);
mpsse_.SetGpio(pin_layout_.sda_out, Mpsse::Direction::OUT, Mpsse::Level::LOW);
mpsse_.GpioWriteCommandToBuffer(index + *bytes_written, buffer, &sub_written);
*bytes_written += sub_written;
mpsse_.SetGpio(pin_layout_.scl, Mpsse::Direction::OUT, Mpsse::Level::HIGH);
mpsse_.SetGpio(pin_layout_.sda_out, Mpsse::Direction::OUT, Mpsse::Level::HIGH);
mpsse_.GpioWriteCommandToBuffer(index + *bytes_written, buffer, &sub_written);
*bytes_written += sub_written;
return ZX_OK;
}
void FtdiI2c::Transact(TransactRequestView request, fdf::Arena& arena,
TransactCompleter::Sync& completer) {
zx_status_t status;
std::vector<uint8_t> write_data(kFtdiI2cMaxTransferSize);
std::vector<uint8_t> read_data(kFtdiI2cMaxTransferSize);
std::vector<fuchsia_hardware_i2cimpl::wire::ReadData> out;
size_t total_read_bytes = 0;
size_t total_write_bytes = 0;
size_t last_stopped_op = 0;
for (size_t i = 0; i < request->op.size(); i++) {
if (request->op[i].type.is_read_size()) {
total_read_bytes += request->op[i].type.read_size();
} else if (request->op[i].type.is_write_data()) {
size_t copy_amt = request->op[i].type.write_data().size();
uint8_t* data_buffer = request->op[i].type.write_data().data();
size_t data_buffer_index = 0;
while (copy_amt--) {
if (total_write_bytes == kFtdiI2cMaxTransferSize) {
return completer.buffer(arena).ReplyError(ZX_ERR_INTERNAL);
}
write_data[total_write_bytes++] = data_buffer[data_buffer_index++];
}
} else {
ZX_ASSERT_MSG(false, "Unknown i2cimpl transfer type");
}
if (request->op[i].stop) {
write_data.resize(total_write_bytes);
read_data.resize(total_read_bytes);
status = Transact(static_cast<uint8_t>(request->op[i].address), write_data, &read_data);
if (status != ZX_OK) {
zxlogf(ERROR, "I2c transact failed with %d", status);
return completer.buffer(arena).ReplyError(status);
}
if (total_read_bytes > 0) {
size_t read_back_index = 0;
for (size_t j = last_stopped_op + 1; j <= i; j++) {
if (request->op[j].type.is_read_size()) {
out.emplace_back().data = fidl::VectorView<uint8_t>(
arena, read_data.data() + read_back_index,
read_data.data() + read_back_index + request->op[j].type.read_size());
read_back_index += request->op[j].type.read_size();
}
}
}
// Reset the write_data for the next transaction.
write_data.resize(kFtdiI2cMaxTransferSize);
read_data.resize(kFtdiI2cMaxTransferSize);
total_write_bytes = 0;
// Reset the read data for the next transaction.
total_read_bytes = 0;
last_stopped_op = i;
}
}
completer.buffer(arena).ReplySuccess(
fidl::VectorView<fuchsia_hardware_i2cimpl::wire::ReadData>::FromExternal(out));
}
void FtdiI2c::handle_unknown_method(
fidl::UnknownMethodMetadata<fuchsia_hardware_i2cimpl::Device> metadata,
fidl::UnknownMethodCompleter::Sync& completer) {
zxlogf(ERROR, "Unknown method %lu", metadata.method_ordinal);
}
zx_status_t FtdiI2c::Create(zx_device_t* device, ftdi_serial::FtdiSerial* const serial,
const fuchsia_hardware_ftdi::wire::I2cBusLayout* layout,
const fuchsia_hardware_ftdi::wire::I2cDevice* i2c_dev) {
// Note: This driver has only been tested on one set of pins.
if (layout->scl != 0 && layout->sda_out != 1 && layout->sda_in != 2) {
return ZX_ERR_OUT_OF_RANGE;
}
ftdi_mpsse::FtdiI2c::I2cLayout i2c_layout;
i2c_layout.scl = layout->scl;
i2c_layout.sda_out = layout->sda_out;
i2c_layout.sda_in = layout->sda_in;
std::vector<ftdi_mpsse::FtdiI2c::I2cDevice> i2c_devices(1);
i2c_devices[0].address = i2c_dev->address;
i2c_devices[0].vid = i2c_dev->vid;
i2c_devices[0].pid = i2c_dev->pid;
i2c_devices[0].did = i2c_dev->did;
auto dev = std::make_unique<ftdi_mpsse::FtdiI2c>(device, serial, i2c_layout, i2c_devices);
zx_status_t status = dev->Bind();
if (status == ZX_OK) {
// devmgr is now in charge of the memory for dev
dev.release();
}
return ZX_OK;
}
} // namespace ftdi_mpsse