Bidirectional test mode signaling over GPIO
* Implements a bidirectional synchronous handshake protocol over
dut_flaga/b GPIOs. This protocol is designed to return true (test
mode) only if the DUT is connected to a servo board which implements
the remote end.
* Includes unit tests for the test mode signaling routine, complete with
mock/fake implementation of the remote end.
Note that we still do not deploy GpioHandler in actual update
processing, which will be done later.
BUG=chromium-os:25397,chromium-os:27109,chromium-os:27672
TEST=Builds and passes unit tests (including new ones)
Change-Id: I265407ed735c3e1354e10782ac30566b16caeb20
Reviewed-on: https://gerrit.chromium.org/gerrit/23330
Reviewed-by: Gaurav Shah <gauravsh@chromium.org>
Tested-by: Gilad Arnold <garnold@chromium.org>
Commit-Ready: Gilad Arnold <garnold@chromium.org>
Reviewed-by: Gilad Arnold <garnold@chromium.org>
diff --git a/gpio_handler.cc b/gpio_handler.cc
index efd5e3d..7145bbb 100644
--- a/gpio_handler.cc
+++ b/gpio_handler.cc
@@ -30,23 +30,30 @@
const StandardGpioHandler::GpioDef
StandardGpioHandler::gpio_defs_[kGpioIdMax] = {
- { "dutflaga", "ID_GPIO_DUTFLAGA" }, // kGpioDutflaga
- { "dutflagb", "ID_GPIO_DUTFLAGB" }, // kGpioDutflagb
+ { "dutflaga", "ID_GPIO_DUTFLAGA" }, // kGpioIdDutflaga
+ { "dutflagb", "ID_GPIO_DUTFLAGB" }, // kGpioIdDutflagb
};
unsigned StandardGpioHandler::num_instances_ = 0;
StandardGpioHandler::StandardGpioHandler(UdevInterface* udev_iface,
- bool is_defer_discovery)
+ FileDescriptor* fd,
+ bool is_defer_discovery,
+ bool is_cache_test_mode)
: udev_iface_(udev_iface),
+ fd_(fd),
+ is_cache_test_mode_(is_cache_test_mode),
is_discovery_attempted_(false) {
- CHECK(udev_iface);
+ CHECK(udev_iface && fd);
// Ensure there's only one instance of this class.
CHECK_EQ(num_instances_, static_cast<unsigned>(0));
num_instances_++;
+ // Reset test signal flags.
+ ResetTestModeSignalingFlags();
+
// If GPIO discovery not deferred, do it.
if (!(is_defer_discovery || DiscoverGpios())) {
LOG(WARNING) << "GPIO discovery failed";
@@ -57,17 +64,29 @@
num_instances_--;
}
-// TODO(garnold) currently, this function always returns false and avoids the
-// GPIO signaling protocol altogether; to be extended later.
bool StandardGpioHandler::IsTestModeSignaled() {
// Attempt GPIO discovery.
if (!DiscoverGpios()) {
LOG(WARNING) << "GPIO discovery failed";
}
- return false;
+ // Force a check if so requested.
+ if (!is_cache_test_mode_)
+ ResetTestModeSignalingFlags();
+
+ bool is_returning_cached = !is_first_check_; // for logging purposes
+ if (is_first_check_) {
+ is_first_check_ = false;
+ DoTestModeSignalingProtocol();
+ }
+
+ LOG(INFO) << "result: " << (is_test_mode_ ? "test" : "normal") << " mode"
+ << (is_returning_cached ? " (cached)" : "")
+ << (is_handshake_completed_ ? "" : " (default)");
+ return is_test_mode_;
}
+
bool StandardGpioHandler::GpioChipUdevEnumHelper::SetupEnumFilters(
udev_enumerate* udev_enum) {
CHECK(udev_enum);
@@ -155,6 +174,23 @@
return true;
}
+StandardGpioHandler::GpioDirResetter::GpioDirResetter(
+ StandardGpioHandler* handler, GpioId id, GpioDir dir) :
+ do_reset_(false), handler_(handler), id_(id), dir_(dir) {
+ CHECK(handler);
+ CHECK_GE(id, 0);
+ CHECK_LT(id, kGpioIdMax);
+ CHECK_GE(dir, 0);
+ CHECK_LT(dir, kGpioDirMax);
+}
+
+StandardGpioHandler::GpioDirResetter::~GpioDirResetter() {
+ if (do_reset_ && !handler_->SetGpioDirection(id_, dir_)) {
+ LOG(WARNING) << "failed to reset direction of " << gpio_defs_[id_].name
+ << " to " << gpio_dirs_[dir_];
+ }
+}
+
bool StandardGpioHandler::InitUdevEnum(struct udev* udev,
UdevEnumHelper* enum_helper) {
@@ -209,6 +245,12 @@
return enum_helper->Finalize();
}
+void StandardGpioHandler::ResetTestModeSignalingFlags() {
+ is_first_check_ = true;
+ is_handshake_completed_ = false;
+ is_test_mode_ = false;
+}
+
bool StandardGpioHandler::DiscoverGpios() {
if (is_discovery_attempted_)
return true;
@@ -251,4 +293,289 @@
return true;
}
+bool StandardGpioHandler::OpenGpioFd(StandardGpioHandler::GpioId id,
+ const char* dev_name,
+ bool is_write) {
+ CHECK(id >= 0 && id < kGpioIdMax && dev_name);
+ string file_name = StringPrintf("%s/%s", gpios_[id].dev_path.c_str(),
+ dev_name);
+ if (!fd_->Open(file_name.c_str(), (is_write ? O_WRONLY : O_RDONLY))) {
+ const string err_str = StringPrintf("failed to open %s (%s) for %s",
+ file_name.c_str(), gpio_defs_[id].name,
+ (is_write ? "writing" : "reading"));
+ if (fd_->IsSettingErrno()) {
+ PLOG(ERROR) << err_str;
+ } else {
+ LOG(ERROR) << err_str;
+ }
+ return false;
+ }
+ return true;
+}
+
+bool StandardGpioHandler::SetGpio(StandardGpioHandler::GpioId id,
+ const char* dev_name, const char* entries[],
+ const int num_entries, int index) {
+ CHECK_GE(id, 0);
+ CHECK_LT(id, kGpioIdMax);
+ CHECK(dev_name);
+ CHECK(entries);
+ CHECK_GT(num_entries, 0);
+ CHECK_GE(index, 0);
+ CHECK_LT(index, num_entries);
+
+ // Open device for writing.
+ if (!OpenGpioFd(id, dev_name, true))
+ return false;
+ ScopedFileDescriptorCloser dev_fd_closer(fd_);
+
+ // Write a string corresponding to the requested output index to the GPIO
+ // device, appending a newline.
+ string output_str = entries[index];
+ output_str += '\n';
+ ssize_t write_len = fd_->Write(output_str.c_str(), output_str.length());
+ if (write_len != static_cast<ssize_t>(output_str.length())) {
+ if (write_len < 0) {
+ const string err_str = "failed to write to GPIO";
+ if (fd_->IsSettingErrno()) {
+ PLOG(ERROR) << err_str;
+ } else {
+ LOG(ERROR) << err_str;
+ }
+ } else {
+ LOG(ERROR) << "wrong number of bytes written (" << write_len
+ << " instead of " << output_str.length() << ")";
+ }
+ return false;
+ }
+
+ // Close the device explicitly, returning the close result.
+ return fd_->Close();
+}
+
+bool StandardGpioHandler::GetGpio(StandardGpioHandler::GpioId id,
+ const char* dev_name, const char* entries[],
+ const int num_entries, int* index_p) {
+ CHECK_GE(id, 0);
+ CHECK_LT(id, kGpioIdMax);
+ CHECK(dev_name);
+ CHECK(entries);
+ CHECK_GT(num_entries, 0);
+ CHECK(index_p);
+
+ // Open device for reading.
+ if (!OpenGpioFd(id, dev_name, false))
+ return false;
+ ScopedFileDescriptorCloser dev_fd_closer(fd_);
+
+ // Read the GPIO device. We attempt to read more than the max number of
+ // characters expected followed by a newline, to ensure that we've indeed read
+ // all the data available on the device.
+ size_t max_entry_len = 0;
+ for (int i = 0; i < num_entries; i++) {
+ size_t entry_len = strlen(entries[i]);
+ if (entry_len > max_entry_len)
+ max_entry_len = entry_len;
+ }
+ max_entry_len++; // account for trailing newline
+ size_t buf_len = max_entry_len + 1; // room for excess char / null terminator
+ char buf[buf_len];
+ memset(buf, 0, buf_len);
+ ssize_t read_len = fd_->Read(buf, buf_len);
+ if (read_len < 0 || read_len > static_cast<ssize_t>(max_entry_len)) {
+ if (read_len < 0) {
+ const string err_str = "failed to read GPIO";
+ if (fd_->IsSettingErrno()) {
+ PLOG(ERROR) << err_str;
+ } else {
+ LOG(ERROR) << err_str;
+ }
+ } else {
+ LOG(ERROR) << "read too many bytes (" << read_len << ")";
+ }
+ return false;
+ }
+
+ // Remove trailing newline.
+ read_len--;
+ if (buf[read_len] != '\n') {
+ LOG(ERROR) << "read value missing trailing newline";
+ return false;
+ }
+ buf[read_len] = '\0';
+
+ // Identify and write GPIO status.
+ for (int i = 0; i < num_entries; i++)
+ if (!strcmp(entries[i], buf)) {
+ *index_p = i;
+ // Close the device explicitly, returning the close result.
+ return fd_->Close();
+ }
+
+ // Oops, unidentified reading...
+ LOG(ERROR) << "read unexpected value from GPIO (`" << buf << "')";
+ return false;
+}
+
+bool StandardGpioHandler::SetGpioDirection(StandardGpioHandler::GpioId id,
+ StandardGpioHandler::GpioDir dir) {
+ return SetGpio(id, "direction", gpio_dirs_, kGpioDirMax, dir);
+}
+
+bool StandardGpioHandler::GetGpioDirection(
+ StandardGpioHandler::GpioId id,
+ StandardGpioHandler::GpioDir* direction_p) {
+ return GetGpio(id, "direction", gpio_dirs_, kGpioDirMax,
+ reinterpret_cast<int*>(direction_p));
+}
+
+bool StandardGpioHandler::SetGpioValue(StandardGpioHandler::GpioId id,
+ StandardGpioHandler::GpioVal value,
+ bool is_check_direction) {
+ // If so instructed, ensure that the GPIO is indeed in the output direction
+ // before attempting to write to it.
+ if (is_check_direction) {
+ GpioDir dir;
+ if (!(GetGpioDirection(id, &dir) && dir == kGpioDirOut)) {
+ LOG(ERROR) << "couldn't verify that GPIO is in the output direction "
+ "prior to reading from it";
+ return false;
+ }
+ }
+
+ return SetGpio(id, "value", gpio_vals_, kGpioValMax, value);
+}
+
+bool StandardGpioHandler::GetGpioValue(StandardGpioHandler::GpioId id,
+ StandardGpioHandler::GpioVal* value_p,
+ bool is_check_direction) {
+ // If so instructed, ensure that the GPIO is indeed in the input direction
+ // before attempting to read from it.
+ if (is_check_direction) {
+ GpioDir dir;
+ if (!(GetGpioDirection(id, &dir) && dir == kGpioDirIn)) {
+ LOG(ERROR) << "couldn't verify that GPIO is in the input direction "
+ "prior to reading from it";
+ return false;
+ }
+ }
+
+ return GetGpio(id, "value", gpio_vals_, kGpioValMax,
+ reinterpret_cast<int*>(value_p));
+}
+
+bool StandardGpioHandler::DoTestModeSignalingProtocol() {
+ // The test mode signaling protocol is designed to provide a robust indication
+ // that a Chrome OS device is physically connected to a servo board in a lab
+ // setting. It is making very few assumptions about the soundness of the
+ // hardware, firmware and kernel driver implementation of the GPIO mechanism.
+ // In general, it is performing a three-way handshake between servo and the
+ // Chrome OS client, based on changes in the GPIO value readings. The
+ // client-side implementation does the following:
+ //
+ // 1. Check for an initial signal (0) on the input GPIO (dut_flaga).
+ //
+ // 2. Flip the signal (1 -> 0) on the output GPIO (dut_flagb).
+ //
+ // 3. Check for a flipped signal (1) on the input GPIO.
+ //
+ // TODO(garnold) the current implementation is based on sysfs access to GPIOs.
+ // We will likely change this to using a specialized in-kernel driver
+ // implementation, which would give us better performance and security
+ // guarantees.
+
+ LOG(INFO) << "attempting GPIO handshake";
+
+ const char* dutflaga_name = gpio_defs_[kGpioIdDutflaga].name;
+ const char* dutflagb_name = gpio_defs_[kGpioIdDutflagb].name;
+
+ // Flip GPIO direction, set it to "in".
+ // TODO(garnold) changing the GPIO direction back and forth is necessary for
+ // overcoming a firmware/kernel issue which causes the device to be in the
+ // "out" state whereas the kernel thinks it is in the "in" state. This should
+ // be abandoned once the firmware and/or kernel driver have been fixed.
+ // Details here: http://code.google.com/p/chromium-os/issues/detail?id=27680
+ if (!(SetGpioDirection(kGpioIdDutflaga, kGpioDirOut) &&
+ SetGpioDirection(kGpioIdDutflaga, kGpioDirIn))) {
+ LOG(ERROR) << "failed to flip direction of input GPIO " << dutflaga_name;
+ return false;
+ }
+
+ // Peek input GPIO state.
+ GpioVal dutflaga_gpio_value;
+ if (!GetGpioValue(kGpioIdDutflaga, &dutflaga_gpio_value, true)) {
+ LOG(ERROR) << "failed to read input GPIO " << dutflaga_name;
+ return false;
+ }
+
+ // If initial handshake signal not received, abort.
+ if (dutflaga_gpio_value != kGpioValDown) {
+ LOG(INFO) << "input GPIO " << dutflaga_name
+ << " unset, terminating handshake";
+ is_handshake_completed_ = true;
+ return true;
+ }
+
+ // Initialize output GPIO to a default state.
+ // TODO(garnold) a similar workaround for possible driver/firmware glitches,
+ // we insist on flipping the direction of the GPIO prior to assuming it is in
+ // the "out" direction.
+ GpioDirResetter dutflagb_dir_resetter(this, kGpioIdDutflagb, kGpioDirIn);
+ if (!(SetGpioDirection(kGpioIdDutflagb, kGpioDirIn) &&
+ dutflagb_dir_resetter.set_do_reset(
+ SetGpioDirection(kGpioIdDutflagb, kGpioDirOut)) &&
+ SetGpioValue(kGpioIdDutflagb, kGpioValUp, false))) {
+ LOG(ERROR) << "failed to initialize output GPIO " << dutflagb_name;
+ return false;
+ }
+
+ // Wait, giving the receiving end enough time to sense the fall.
+ sleep(kServoOutputResponseWaitInSecs);
+
+ // Flip the output signal.
+ if (!SetGpioValue(kGpioIdDutflagb, kGpioValDown, false)) {
+ LOG(ERROR) << "failed to flip output GPIO " << dutflagb_name;
+ return false;
+ }
+
+ // Look for flipped input GPIO value, up to a preset timeout.
+ Time expires =
+ Time::Now() + TimeDelta::FromSeconds(kServoInputResponseTimeoutInSecs);
+ TimeDelta delay =
+ TimeDelta::FromMicroseconds(1000000 / kServoInputNumChecksPerSec);
+ bool is_first_response_check = true;
+ bool is_error = false;
+ while (Time::Now() < expires) {
+ if (is_first_response_check)
+ is_first_response_check = false;
+ else
+ usleep(delay.InMicroseconds());
+
+ // Read input GPIO.
+ if (!GetGpioValue(kGpioIdDutflaga, &dutflaga_gpio_value, true)) {
+ LOG(ERROR) << "failed to read input GPIO " << dutflaga_name;
+ is_error = true;
+ break;
+ }
+
+ // If dutflaga is now up (flipped), we got our signal!
+ if (dutflaga_gpio_value == kGpioValUp) {
+ is_test_mode_ = true;
+ break;
+ }
+ }
+
+ if (!is_error) {
+ if (is_test_mode_) {
+ is_handshake_completed_ = true;
+ LOG(INFO) << "GPIO handshake completed, test mode signaled";
+ } else {
+ LOG(INFO) << "timed out waiting for input GPIO " << dutflaga_name
+ << " to flip, terminating handshake";
+ }
+ }
+
+ return is_handshake_completed_;
+}
+
} // namespace chromeos_update_engine