A character driver for the K400/HX300 PSE over ISHTP
The Elkhart Lake Programmable Services Engine can be managed over ISHTP/HECI from the host processor.
This driver creates and manages a pse character device, which allows userspace applications to read and
write data over the underlying ISHTP bus driver.
-
A Karbon 400-series or Helix 300-series system.
-
An installation of Ubuntu for Intel IoT
|
Important
|
The pinctrl-elkhartlake driver will timeout during startup and crash during shutdown, until a BIOS update
is available that addresses the issue, it is recommended that you disable this driver module.
(For example, add modprobe.blacklist=pinctrl-elkartlake to the kernel command line.)
|
To build and install the pse kernel module automatically, simply run:
$ sudo chmod +x install.sh && sudo ./install.shThis will build and install PSE kernel module with DKMK. If "PSE kernel module installed!" is shown, it is ready to use. Notice that the below step is required after every OS reboot, then the device can be opened for communication again.
$ sudo modprobe pseIf all goes well, you can move on to Usage
It is also possible to upgrade Linux kernel by user. Most of time, the corresponding PSE kernel module can be automatic installed after reboot.
$ uname -r
$ sudo dkms statusIf no matched version’s PSE installation can be found, please try to rebuild and reinstall by script.
$ sudo chmod +x install.sh && sudo ./install.shIf "PSE kernel module installed!" has been shown, you can move on to Usage If "PSE kernel module installation failed!" and "Your kernel headers cannot be found" are shown, it is because the corresponding header cannot be found or the build packages have unmet dependencies. It is recommended to install another Linux kernel version.
It is possible to interface with the PSE directly from your software
stack by reading and writing to the PSE character file (/dev/pse). A complete example that establishes a connection
to the PSE and reads the firmware version information is included in the examples directory, and can be built with:
$ cd examples
$ sudo chmod +x examples.sh && sudo ./examples.shAll source code referenced in the instructions below is included in full context in the examples directory. Additional example code is provided for using the CAN and DIO peripherals, as well as sample code for configuring the system’s
automotive features.
Before communication with the PSE can begin, the host client must establish a connection with the firmware client. This is performed by sending the 'client connection' IOCTL to the device file:
[<start> start] -> [<abstract> /dev/pse] [/dev/pse] - [<state> open] [open] -> connecting requires root [<choice> root?] [root?] -> Yes [<state> IOCTL: ISHTP Connect Client] [root?] No -> [<end> end] [<reference> PSE SMHI UUID] -> [IOCTL: ISHTP Connect Client] [IOCTL: ISHTP Connect Client] -> [Connection] |
/// ... int pse_client_connect(void)
int fd, ret;
struct ishtp_cc_data cc_data;
/// Prep input connection data
memcpy(&(cc_data.in_client_uuid), &pse_smhi_guid, sizeof(pse_smhi_guid));
/// Open the pse character device (fails if not root)
fd = open(PSE_CHRDEV, O_RDWR);
if (fd <= 0) {
printf("Failed to open the pse device file\n");
return fd;
}
/// Send the connection IOCTL
ret = ioctl(fd, IOCTL_ISHTP_CONNECT_CLIENT, &cc_data);
if (ret) {
printf("Failed to connect to the PSE over ISHTP/HECI\n");
return ret;
}
/// Return the file descriptor for use
return fd; |
Once the connection has been established and returned, it is possible to send commands to the PSE.
Commands sent to the programmable services engine can either be header-only 'short' commands, or complete header + body 'long' commands.
The header portion of the command is as 48-bit wide structure that is transmitted as raw data to the PSE:
{signal: [
{name: 'bits', wave: 'x.1hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhx.'},
{name: 'data', wave: 'x.6...............................................x.', data: ['header']},
{name: 'send', wave: 'x.3.......x.......5.......6...............x.......x.', data: ['command', 'has next', 'argument']},
{name: 'recv', wave: 'x.3.......4.......5.......6...............7.......x.', data: ['command', 'is response', 'has next', 'argument', 'status']}
]}
|
Note
|
When sending a command the is response and status bytes are ignored; these are only set by the firmware
endpoint when sending data back to the host.
|
The command header can be easily described as a packed struct in C:
typedef struct {
// The command 'class' of this message
uint8_t command;
// True when the message is a response from the PSE
bool is_response;
// If this command has follow-on body data
bool has_next;
// The packed command argument; exact data depends on the command
uint16_t argument;
// Status of the received command when responding; zero on success
uint8_t status;
} __packed heci_header_t;The possible valid system commands are enumerated by the heci_command_id_t, and include:
| Command | Value | Description |
|---|---|---|
System Info |
|
Report the system firmware version number |
Digital IO |
|
Set and read the system’s digital IO and LEDs |
UART |
|
Send data over an attached UART. Allows control of automotive features |
CAN Bus |
|
Send and receive CAN messages |
PWM |
|
Manage the Pulse Width Modulation of a DIO configured as PWM |
I2C |
|
Send/receive I2C data (not recommended for end-user configuration) |
QEP |
|
Configure a group of DIOs set as a Quadrature Encoder Peripheral |
The argument packing depends on this command, and is described by the *_command_t structures.
UART, I2C, PWM, and QEP all use a generic 'operation + device' scheme, while DIO and CAN have separate formats:
{signal: [
{name: 'bits', wave: 'x.1hhhhhhhhhhhhhhhx.'},
{name: 'data', wave: 'x.6...............x.', data: ['argument']},
{name: 'gen', wave: 'x.3.......4.......x.', data: ['operation', 'device']},
{name: 'dio', wave: 'x.3.......4...5...x.', data: ['operation', 'device', 'pin number']},
{name: 'can', wave: 'x.3..4..5.........x.', data: ['operation', 'device', 'argument (baudrate)']},
]}
The 'body' portion of each command is described by another structure:
typedef struct {
/// Indicates the format of the following data
heci_data_kind_t kind: 8;
/// Length, in bytes, of the data portion
uint32_t length;
uint32_t padding;
/// Packed message body data
uint8_t data[MAX_HECI_DATA_LEN];
} __packed heci_body_t;The actual data format depends on the command sent; the
can_send example function in examples/can.c shows one method for populating message data.
Once a message’s header and optional body portions are prepared, sending the command is as simple as writing to
the open PSE device:
[<start> start] - [<state> send] [send] -> [<choice> short command?] [<abstract> /dev/pse] -- [Connection] [short command?] No -> [<state> send header] [short command?] Yes -> [<state> send header + body] [send header] -> [Connection] [send header + body] -> [Connection] [Connection] -> [<state> write] [write] -> [<end> end] |
/// pse_send_command(int fd, heci_command_id_t command, uint16_t data, heci_body_t * body)
size_t len;
/// Create the initial header
heci_header_t header = {
.status = 0,
.is_response = 0,
.has_next = body != NULL ? 1 : 0,
.command = (uint8_t)command,
.argument = data
};
/// Copy the header into the transmit buffer
memcpy(heci_tx_buffer, &header, sizeof(heci_header_t));
/// Add a body to the command if one is present
if (header.has_next) {
memcpy(heci_tx_buffer + sizeof(heci_header_t), body, sizeof(heci_body_t));
len = sizeof(heci_header_t) + sizeof(heci_body_t);
} else {
len = sizeof(heci_header_t);
}
write(fd, heci_tx_buffer, len); |
Any message sent to the PSE will receive a response from the embedded controller. The response message format is identical to the transmit format, and will always include the status of the last command received.
Some commands (like reading a CAN message), will result in additional data being returned, as indicated by the
has_next flag. Application code may check this flag to determine if it should continue reading data:
#padding: 12 #spacing: 25 [<abstract> /dev/pse] -- [Connection] [Connection] -> [<state> read header] [Connection] --> [<state> read body] [<start> start] - [<state> recv] [recv] -> [Connection] [read header] -> [<choice> has next?] [has next?] No -> [<end> exit] [has next?] -> Yes [read body] [read body] -> [exit] |
/// pse_read_response(int fd, heci_header_t *header, heci_body_t *body)
/// NOTE: Some file preparation occurs here; check sample code
/// Read the message header
length = read(fd, data, sizeof(heci_header_t));
if (length <= 0 || length != sizeof(heci_header_t)) {
printf("Failed reading header from the pse file (%i)\n", length);
return length;
}
memcpy(header, data, sizeof(heci_header_t));
/// If the header has followup data, read it
if (header->has_next && !body) {
printf("Warning: Returned body data was dropped!\n");
return length;
} else if (header->has_next) {
length = read(fd, data + sizeof(heci_header_t), sizeof(heci_body_t));
if (length <= 0 || length != sizeof(heci_body_t)) {
printf("Failed reading body from the pse file (%i)\n", length);
return length;
}
memcpy(body, data + sizeof(heci_header_t), sizeof(heci_body_t));
length = sizeof(heci_body_t) + sizeof(heci_header_t);
} else {
length = sizeof(heci_header_t);
} |
In the provided sample code, the files examples/pse.c and examples/pse.h provide some helpful abstraction to
simplify this process. For instance, reading the PSE firmware version can be performed with the following code:
link:./examples/version.c[role=include]To add a new example source file (such as custom_code.c) to the build within the examples/ directory:
-
Place custom_code.c in the examples directory.
-
Add the required dependencies in the linkage step of the Makefile:
custom_code: pse.o custom_code.o cc -o custom_code custom_code.o pse.o -
Update the all target in the Makefile to build the new example:
all: ... ${MAKE} custom_code -
Add custom_code to the clean target to ensure proper removal during rebuilds:
rm -rf ... custom_code
After running examples.sh, a new executable named custom_code will be created if the build completes successfully.
Edit custom_code.c and re-run examples.sh to continue developing the custom application.