blob: b5a19df0aca1d3b1585fd14f5261fb06a381ba32 [file] [log] [blame]
// SPDX-License-Identifier: BSD-3-Clause
//
// Copyright 2024 Google LLC
#include <common.h>
#include <efi_loader.h>
#include <malloc.h>
#include <mmc.h>
#include <aml_sd_emmc.h>
#define VENDOR_PATH_DATA_LEN 16
// Represents the EFI device path for a hardware partition on a Amlogic SD
// eMMC.
struct efi_aml_sd_disk_hwpart_device_path {
struct efi_device_path_vendor path;
char vendor_defined_data[VENDOR_PATH_DATA_LEN];
struct efi_device_path end;
};
// Contains input parameters for block read/write.
struct efi_io2_request {
void *buffer;
uint64_t blk_off;
uint64_t blk_cnt;
bool is_write;
efi_block_io2_token_t *token;
bool io_started;
};
// Represents a hardware partition on an Amlogic SD eMMC device.
struct efi_aml_sd_disk_hwpart {
struct efi_object header;
struct efi_aml_sd_disk *disk;
int hwpart;
struct efi_aml_sd_disk_hwpart_device_path dp;
struct efi_block_io_media media;
struct efi_block_io ops;
struct efi_block_io2 ops_io2;
struct efi_io2_request io_request;
};
// Represents an Amlogic SD eMMC device.
struct efi_aml_sd_disk {
struct mmc *mmc;
int dev;
efi_block_io2_token_t *io2_token;
struct efi_event *timer_event;
struct efi_aml_sd_disk_hwpart hwparts[3];
int curr_hwpart; // current hardware partition to process.
efi_status_t status;
};
// Sets an EFI_BLOCK_IO2 request to the finished state. Sets status and
// notifies completion.
static void efi_io2_finish(struct efi_io2_request *request, efi_status_t status)
{
if (!request->buffer || !request->token) {
return;
}
if (request->token->event) {
request->token->event->is_signaled = true;
}
request->token->transaction_status = status;
// Resets everything to stop referencing user buffer.
memset(request, 0, sizeof(*request));
}
// Aborts all IO with the error code.
static void abort_all_ios(struct efi_aml_sd_disk *disk, efi_status_t err) {
for (size_t i = 0; i < ARRAY_SIZE(disk->hwparts); i++) {
efi_io2_finish(&disk->hwparts[i].io_request, err);
}
}
// Sets disk to an error state and aborts all IO with the error code.
static void handle_driver_error(struct efi_aml_sd_disk *disk, efi_status_t err)
{
disk->status = err;
abort_all_ios(disk, err);
}
// Processes pending IO requests.
static void efi_disk_poll(struct efi_aml_sd_disk *disk)
{
// We don't support recovering from previous driver error.
if (disk->status != EFI_SUCCESS) {
return;
}
size_t num_hwparts = ARRAY_SIZE(disk->hwparts);
// Process the IO request of currently scheduled hardwared partition.
struct efi_io2_request *request =
&disk->hwparts[disk->curr_hwpart].io_request;
// If the hardware partition doesn't have a pending request, find the next
// one.
for (size_t i = 0; i < num_hwparts && !request->buffer; i++) {
disk->curr_hwpart = (disk->curr_hwpart + 1) % num_hwparts;
request = &disk->hwparts[disk->curr_hwpart].io_request;
}
// Returns early if none has any IO request.
if (!request->buffer) {
return;
}
// Processes/updates the request.
if (!request->io_started) {
if (aml_sd_is_busy(disk->mmc)) {
return;
}
// Initiates the read/write if it has not started.
mmc_select_hwpart(disk->dev, disk->curr_hwpart);
if (request->is_write ?
aml_sd_write_async(disk->mmc, request->blk_off,
request->blk_cnt,
request->buffer) :
aml_sd_read_async(disk->mmc, request->blk_off,
request->blk_cnt,
request->buffer)) {
handle_driver_error(disk, EFI_DEVICE_ERROR);
} else {
request->io_started = true;
}
} else {
// If started, polls emmc status.
if (aml_sd_poll_io(disk->mmc)) {
handle_driver_error(disk, EFI_DEVICE_ERROR);
} else if (!aml_sd_is_busy(disk->mmc)) {
efi_io2_finish(request, EFI_SUCCESS);
// Sets to the next hardware partition to poll.
// This ensures fairness and prevents one busy block device from
// getting scheduled all the time.
disk->curr_hwpart = (disk->curr_hwpart + 1) % num_hwparts;
}
}
}
// Common helper used by `read_blocks_ex` and `write_blocks_ex`.
static efi_status_t EFIAPI rw_blocks_ex(struct efi_block_io2 *this,
uint32_t media_id, uint64_t lba,
efi_block_io2_token_t *token,
size_t buffer_size, void *buffer,
bool is_write)
{
if (!this) {
return EFI_INVALID_PARAMETER;
}
struct efi_aml_sd_disk_hwpart *disk_hwpart =
container_of(this, struct efi_aml_sd_disk_hwpart, ops_io2);
// Checks parameters.
uint64_t blk_cnt = buffer_size / this->media->block_size;
uint64_t end = lba + blk_cnt;
if (buffer_size % this->media->block_size) {
return EFI_BAD_BUFFER_SIZE;
} else if (end < lba || end > this->media->last_block + 1 ||
((size_t)buffer % (size_t)this->media->io_align)) {
return EFI_INVALID_PARAMETER;
}
EFI_ENTRY("%p, %x, %llx, %zx, %p", this, media_id, lba, buffer_size,
buffer);
// Poll the eMMC, in case this device has an IO but is already finished.
// This will clear the request and therefore allow us to serve the new one
// instead of returnning `EFI_OUT_OF_RESOURCES`
efi_timer_check();
// If there is already a pending request, returns EFI_OUT_OF_RESOURCES.
if (disk_hwpart->io_request.buffer) {
return EFI_EXIT(EFI_OUT_OF_RESOURCES);
}
disk_hwpart->io_request.buffer = buffer;
disk_hwpart->io_request.blk_off = lba;
disk_hwpart->io_request.blk_cnt = blk_cnt;
disk_hwpart->io_request.is_write = is_write;
disk_hwpart->io_request.token = token;
disk_hwpart->io_request.io_started = false;
// Perform a poll to catch early errors.
efi_timer_check();
// If operation is blocking, waits for the IO to complete.
if (token->event == NULL) {
// `efi_disk_poll` resets buffer pointer to NULL once completed.
while (disk_hwpart->io_request.buffer) {
efi_timer_check();
}
}
return EFI_EXIT(disk_hwpart->disk->status);
}
// `EFI_BLOCK_IO2_PROTOCOL.read_blocks_ex`.
static efi_status_t EFIAPI read_blocks_ex(struct efi_block_io2 *this,
uint32_t media_id, uint64_t lba,
efi_block_io2_token_t *token,
size_t buffer_size, void *buffer)
{
return rw_blocks_ex(this, media_id, lba, token, buffer_size, buffer,
false);
}
// `EFI_BLOCK_IO2_PROTOCOL.write_blocks_ex`.
static efi_status_t EFIAPI write_blocks_ex(struct efi_block_io2 *this,
uint32_t media_id, uint64_t lba,
efi_block_io2_token_t *token,
size_t buffer_size, void *buffer)
{
return rw_blocks_ex(this, media_id, lba, token, buffer_size, buffer,
true);
}
// `EFI_BLOCK_IO2_PROTOCOL.flush_blocks`.
static efi_status_t EFIAPI flush_blocks_ex(struct efi_block_io2 *this,
efi_block_io2_token_t *token)
{
EFI_ENTRY("%p", this);
token->transaction_status = EFI_SUCCESS;
if (token->event) {
token->event->is_signaled = true;
}
return EFI_EXIT(EFI_SUCCESS);
}
// `EFI_BLOCK_IO2_PROTOCOL.reset`.
static efi_status_t EFIAPI reset_io2(struct efi_block_io2 *this,
bool extended_verification)
{
if (!this) {
return EFI_INVALID_PARAMETER;
}
EFI_ENTRY("%p, %x", this, extended_verification);
struct efi_aml_sd_disk_hwpart *disk_hwpart =
container_of(this, struct efi_aml_sd_disk_hwpart, ops_io2);
struct efi_aml_sd_disk *disk = disk_hwpart->disk;
// Waits until the current mmc operation is finished. Disregards result.
aml_sd_sync(disk->mmc);
// Aborts all IO.
abort_all_ios(disk, EFI_ABORTED);
return EFI_EXIT(EFI_SUCCESS);
}
// Common helper used by `read_blocks` and `write_blocks`.
static efi_status_t EFIAPI rw_blocks(struct efi_block_io *this,
uint32_t media_id, uint64_t lba,
size_t buffer_size, void *buffer,
bool is_write)
{
if (!this) {
return EFI_INVALID_PARAMETER;
}
// Redirects to `EFI_BLOCK_IO2_PROTOCOL` APIs.
struct efi_aml_sd_disk_hwpart *disk_hwpart =
container_of(this, struct efi_aml_sd_disk_hwpart, ops);
efi_block_io2_token_t token = {
.event = NULL,
.transaction_status = EFI_NOT_READY,
};
return is_write ? write_blocks_ex(&disk_hwpart->ops_io2, media_id, lba,
&token, buffer_size, buffer) :
read_blocks_ex(&disk_hwpart->ops_io2, media_id, lba,
&token, buffer_size, buffer);
}
// `EFI_BLOCK_IO_PROTOCOL.read_blocks`.
static efi_status_t EFIAPI read_blocks(struct efi_block_io *this, u32 media_id,
u64 lba, efi_uintn_t buffer_size,
void *buffer)
{
return rw_blocks(this, media_id, lba, buffer_size, buffer, false);
}
// `EFI_BLOCK_IO_PROTOCOL.write_blocks`.
static efi_status_t EFIAPI write_blocks(struct efi_block_io *this, u32 media_id,
u64 lba, efi_uintn_t buffer_size,
void *buffer)
{
return rw_blocks(this, media_id, lba, buffer_size, buffer, true);
}
// `EFI_BLOCK_IO_PROTOCOL.reset`.
static efi_status_t EFIAPI reset(struct efi_block_io *this,
char extended_verification)
{
if (!this) {
return EFI_INVALID_PARAMETER;
}
// Redirects to `EFI_BLOCK_IO2_PROTOCOL` APIs.
struct efi_aml_sd_disk_hwpart *disk_hwpart =
container_of(this, struct efi_aml_sd_disk_hwpart, ops);
return reset_io2(&disk_hwpart->ops_io2, extended_verification);
}
// `EFI_BLOCK_IO_PROTOCOL.flush_blocks`.
static efi_status_t EFIAPI flush_blocks(struct efi_block_io *this)
{
if (!this) {
return EFI_INVALID_PARAMETER;
}
// Redirects to `EFI_BLOCK_IO2_PROTOCOL` APIs.
struct efi_aml_sd_disk_hwpart *disk_hwpart =
container_of(this, struct efi_aml_sd_disk_hwpart, ops);
efi_block_io2_token_t token = {
.event = NULL,
.transaction_status = EFI_NOT_READY,
};
return flush_blocks_ex(&disk_hwpart->ops_io2, &token);
}
// A timer event notification function that polls and updates block device
// status.
static void EFIAPI aml_sd_disk_timer_notify(struct efi_event *event,
void *context)
{
EFI_ENTRY("%p, %p", event, context);
efi_disk_poll(context);
EFI_EXIT(EFI_SUCCESS);
}
efi_status_t efi_aml_sd_disk_register(void)
{
struct mmc *mmc = find_mmc_device(CONFIG_FASTBOOT_FLASH_MMC_DEV);
if (!is_aml_sd_mmc(mmc)) {
return EFI_UNSUPPORTED;
}
printf("Adding EFI Amlogic SD eMMC disk..\n");
struct efi_aml_sd_disk *disk =
calloc(1, sizeof(struct efi_aml_sd_disk));
if (!disk)
return EFI_OUT_OF_RESOURCES;
disk->mmc = mmc;
disk->dev = CONFIG_FASTBOOT_FLASH_MMC_DEV;
// Creates a block device for boot0/boot1/user hardware partitions.
for (size_t hwpart = 0; hwpart < ARRAY_SIZE(disk->hwparts); hwpart++) {
struct efi_aml_sd_disk_hwpart *disk_hwpart =
&disk->hwparts[hwpart];
// Adds to the device list
efi_add_handle(&disk_hwpart->header);
disk_hwpart->disk = disk;
disk_hwpart->hwpart = hwpart;
// Creates a vendor type device path.
disk_hwpart->dp.path.dp.type = DEVICE_PATH_TYPE_MEDIA_DEVICE;
disk_hwpart->dp.path.dp.sub_type =
DEVICE_PATH_SUB_TYPE_VENDOR_PATH;
disk_hwpart->dp.path.dp.length =
sizeof(disk_hwpart->dp.path) + VENDOR_PATH_DATA_LEN;
snprintf(disk_hwpart->dp.vendor_defined_data,
VENDOR_PATH_DATA_LEN, "hwpart-%ld", hwpart);
disk_hwpart->dp.end.type = DEVICE_PATH_TYPE_END;
disk_hwpart->dp.end.sub_type = DEVICE_PATH_SUB_TYPE_END;
// Populates `EFI_BLOCK_IO_MEDIA`
disk_hwpart->media.media_id = 1; // Don't care.
disk_hwpart->media.removable_media = 0;
disk_hwpart->media.media_present = 1;
disk_hwpart->media.logical_partition = 0;
disk_hwpart->media.read_only = 0;
disk_hwpart->media.write_caching = 0;
disk_hwpart->media.block_size = mmc->write_bl_len;
disk_hwpart->media.io_align = mmc->write_bl_len;
uint64_t total_blocks =
(hwpart == 0 ? mmc->capacity_user : mmc->capacity_boot);
total_blocks /= mmc->write_bl_len;
disk_hwpart->media.last_block = max(1ULL, total_blocks) - 1;
// Populates `EFI_BLOCK_IO2_PROTOCOL`.
disk_hwpart->ops_io2.media = &disk_hwpart->media;
disk_hwpart->ops_io2.reset = reset_io2;
disk_hwpart->ops_io2.read_blocks_ex = read_blocks_ex;
disk_hwpart->ops_io2.write_blocks_ex = write_blocks_ex;
disk_hwpart->ops_io2.flush_blocks_ex = flush_blocks_ex;
// Populates `EFI_BLOCK_IO_PROTOCOL`.
disk_hwpart->ops.revision = 1;
disk_hwpart->ops.media = &disk_hwpart->media;
disk_hwpart->ops.reset = reset;
disk_hwpart->ops.read_blocks = read_blocks;
disk_hwpart->ops.write_blocks = write_blocks;
disk_hwpart->ops.flush_blocks = flush_blocks;
// Installs protocols
struct efi_object *handle = &disk_hwpart->header;
efi_status_t ret = efi_install_multiple_protocol_interfaces(
&handle, &efi_guid_device_path,
&disk_hwpart->dp.path.dp, &efi_block_io_guid,
&disk_hwpart->ops, &efi_block_io2_guid,
&disk_hwpart->ops_io2, NULL);
if (ret != EFI_SUCCESS) {
return ret;
}
}
// Creates a timer event so that the notify function can get triggered
// whenever the EFI boot service processes any event.
efi_status_t ret = efi_create_event(EVT_TIMER | EVT_NOTIFY_SIGNAL,
TPL_NOTIFY,
aml_sd_disk_timer_notify, disk,
NULL, &disk->timer_event);
if (ret != EFI_SUCCESS) {
printf("Failed to create timer event for EFI AML disk\n");
return ret;
}
// Set timer period to 0 to trigger the event in every timer cycle.
// This is equivalent to invoking `aml_sd_disk_timer_notify` every time
// boot service checks any event i.e. in `efi_timer_check()`.
ret = efi_set_timer(disk->timer_event, EFI_TIMER_PERIODIC, 0);
if (ret != EFI_SUCCESS) {
printf("Failed to set timer for EFI AML disk\n");
return ret;
}
return EFI_SUCCESS;
}