| // 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; |
| } |