// Copyright 2024 The Pigweed Authors // // 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 // // https://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 #include #include #include #include #include "pw_log/log.h" #include "pw_preprocessor/util.h" #include "pw_result/result.h" #include "pw_spi_linux/spi.h" #include "pw_status/status.h" namespace pw::spi { namespace { constexpr unsigned int kDefaultMode = 0; constexpr unsigned int kDefaultBits = 8; const struct option kLongOptions[] = { {"bits", required_argument, nullptr, 'b'}, {"device", required_argument, nullptr, 'D'}, {"freq", required_argument, nullptr, 'F'}, {"human", no_argument, nullptr, 'h'}, {"input", required_argument, nullptr, 'i'}, {"lsb", no_argument, nullptr, 'l'}, {"mode", required_argument, nullptr, 'm'}, {"output", required_argument, nullptr, 'o'}, {"rx-count", required_argument, nullptr, 'r'}, {}, // terminator }; // Starts with ':' to skip printing errors and return ':' for a missing option // argument. const char* kShortOptions = ":b:D:F:hi:lm:o:r:"; void Usage() { std::cerr << "Usage: pw_spi_linux_cli -D DEVICE -F FREQ [flags]" << std::endl; std::cerr << "Required flags:" << std::endl; std::cerr << " -D/--device SPI device path (e.g. /dev/spidev0.0" << std::endl; std::cerr << " -F/--freq SPI clock frequency in Hz (e.g. 24000000)" << std::endl; std::cerr << std::endl; std::cerr << "Optional flags:" << std::endl; std::cerr << " -b/--bits Bits per word, default: " << kDefaultBits << std::endl; std::cerr << " -h/--human Human-readable output (default: binary, " "unless output to stdout tty)" << std::endl; std::cerr << " -i/--input Input file, or - for stdin" << std::endl; std::cerr << " If not given, no data is sent." << std::endl; std::cerr << " -l/--lsb LSB first (default: MSB first)" << std::endl; std::cerr << " -m/--mode SPI mode (0-3), default: " << kDefaultMode << std::endl; std::cerr << " -o/--output Output file (default: stdout)" << std::endl; std::cerr << " -r/--rx-count Number of bytes to receive (defaults to size " "of input)" << std::endl; } struct Args { std::string device; unsigned int frequency = 0; std::optional input_path; std::string output_path = "-"; bool human_readable = false; std::optional rx_count; unsigned int mode = kDefaultMode; unsigned int bits = kDefaultBits; bool lsb_first = false; Config GetSpiConfig() const { return { .polarity = (mode & 0b10) ? ClockPolarity::kActiveLow : ClockPolarity::kActiveHigh, .phase = (mode & 0b01) ? ClockPhase::kFallingEdge : ClockPhase::kRisingEdge, .bits_per_word = bits, .bit_order = lsb_first ? BitOrder::kLsbFirst : BitOrder::kMsbFirst, }; } }; template std::optional ParseNumber(std::string_view str) { T value{}; const auto* str_end = str.data() + str.size(); auto [ptr, ec] = std::from_chars(str.data(), str_end, value); if (ec == std::errc() && ptr == str_end) { return value; } return std::nullopt; } Result ParseArgs(int argc, char* argv[]) { Args args; bool human_readable_given; while (true) { int current_optind = optind; int c = getopt_long(argc, argv, kShortOptions, kLongOptions, nullptr); if (c == -1) { break; } switch (c) { case 'b': { auto bits = ParseNumber(optarg); if (bits > 32) { PW_LOG_ERROR("Invalid bits : %s", optarg); return Status::InvalidArgument(); } args.bits = bits.value(); break; } case 'D': args.device = optarg; break; case 'F': { auto freq = ParseNumber(optarg); if (!freq || freq.value() == 0) { PW_LOG_ERROR("Invalid frequency: %s", optarg); return Status::InvalidArgument(); } args.frequency = freq.value(); break; } case 'h': human_readable_given = true; break; case 'i': args.input_path = optarg; break; case 'l': args.lsb_first = true; break; case 'm': { auto mode = ParseNumber(optarg); if (!mode || mode.value() > 3) { PW_LOG_ERROR("Invalid mode: %s", optarg); return Status::InvalidArgument(); } args.mode = mode.value(); break; } case 'o': args.output_path = optarg; break; case 'r': { auto count = ParseNumber(optarg); if (!count) { PW_LOG_ERROR("Invalid count: %s", optarg); return Status::InvalidArgument(); } args.rx_count = count; break; } case '?': if (optopt) { PW_LOG_ERROR("Invalid flag: -%c", optopt); } else { PW_LOG_ERROR("Invalid flag: %s", argv[current_optind]); } Usage(); return Status::InvalidArgument(); case ':': PW_LOG_ERROR("Missing argument to %s", argv[current_optind]); return Status::InvalidArgument(); } } args.human_readable = human_readable_given || (args.output_path == "-" && isatty(STDOUT_FILENO)); // Check for required flags if (args.device.empty()) { PW_LOG_ERROR("Missing required flag: -D/--device"); Usage(); return Status::InvalidArgument(); } if (!args.frequency) { PW_LOG_ERROR("Missing required flag: -F/--frequency"); Usage(); return Status::InvalidArgument(); } // Either input file or rx count must be provided if (!args.input_path && !args.rx_count) { PW_LOG_ERROR("Either -i/--input or -r/--rx must be provided."); return Status::InvalidArgument(); } return args; } std::vector ReadInput(const std::string& path, size_t limit) { std::ifstream input_file; if (path != "-") { input_file.open(path, std::ifstream::in); if (!input_file.is_open()) { PW_LOG_ERROR("Failed to open %s", path.c_str()); exit(2); } } std::istream& instream = input_file.is_open() ? input_file : std::cin; std::vector result; for (size_t i = 0; i < limit; i++) { int b = instream.get(); if (b == EOF) { break; } result.push_back(static_cast(b)); } return result; } void WriteOutput(const std::string& path, std::vector data, bool human_readable) { std::ofstream output_file; if (path != "-") { output_file.open(path, std::ifstream::out); if (!output_file.is_open()) { PW_LOG_ERROR("Failed to open %s", path.c_str()); exit(2); } } std::ostream& out = output_file.is_open() ? output_file : std::cout; if (human_readable) { out << '"'; } for (std::byte b : data) { char c = static_cast(b); if (!human_readable || std::isprint(c)) { out.put(c); } else if (c == '\0') { out << "\\0"; } else if (c == '\n') { out << "\\n"; } else { out << "\\x" << std::hex << std::setfill('0') << std::setw(2) << static_cast(c); } } if (human_readable) { out << '"' << std::endl; } } int MainInNamespace(int argc, char* argv[]) { auto maybe_args = ParseArgs(argc, argv); if (!maybe_args.ok()) { return 1; } auto args = std::move(maybe_args.value()); int fd = open(args.device.c_str(), O_RDWR); if (fd < 0) { PW_LOG_ERROR("Failed to open %s: %s", args.device.c_str(), strerror(errno)); return 1; } PW_LOG_DEBUG("Opened %s", args.device.c_str()); // Set up SPI Initiator. LinuxInitiator initiator(fd, args.frequency); if (auto status = initiator.Configure(args.GetSpiConfig()); !status.ok()) { PW_LOG_ERROR( "Failed to configure %s: %s", args.device.c_str(), status.str()); return 2; } PW_LOG_DEBUG("Configured %s", args.device.c_str()); // Read input data for transmit. std::vector tx_data; if (args.input_path) { tx_data = ReadInput(args.input_path.value(), 1024); } // Set up receive buffer. std::vector rx_data(args.rx_count ? args.rx_count.value() : tx_data.size()); // Perform a transfer! PW_LOG_DEBUG( "Ready to send %zu, receive %zu bytes", tx_data.size(), rx_data.size()); if (auto status = initiator.WriteRead(tx_data, rx_data); !status.ok()) { PW_LOG_ERROR("Failed to send/recv data: %s", status.str()); return 2; } PW_LOG_DEBUG("Transfer successful! (%zu bytes)", rx_data.size()); WriteOutput(args.output_path, rx_data, args.human_readable); return 0; } } // namespace } // namespace pw::spi int main(int argc, char* argv[]) { return pw::spi::MainInNamespace(argc, argv); }