mirror of
				https://git.proxmox.com/git/fwupd
				synced 2025-10-31 17:20:33 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			397 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			397 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /*
 | |
|  * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
 | |
|  *
 | |
|  * SPDX-License-Identifier: LGPL-2.1+
 | |
|  */
 | |
| 
 | |
| #include "config.h"
 | |
| 
 | |
| #include <math.h>
 | |
| #include <stdio.h>
 | |
| #include <string.h>
 | |
| 
 | |
| #include "dfu-common.h"
 | |
| #include "dfu-sector.h"
 | |
| #include "dfu-target-stm.h"
 | |
| #include "dfu-target-private.h"
 | |
| 
 | |
| #include "fwupd-error.h"
 | |
| 
 | |
| G_DEFINE_TYPE (DfuTargetStm, dfu_target_stm, DFU_TYPE_TARGET)
 | |
| 
 | |
| /* STMicroelectronics STM32 version of DFU:
 | |
|  * www.st.com/resource/en/application_note/cd00264379.pdf */
 | |
| #define DFU_STM_CMD_GET_COMMAND			0x00
 | |
| #define DFU_STM_CMD_SET_ADDRESS_POINTER		0x21
 | |
| #define DFU_STM_CMD_ERASE			0x41
 | |
| #define DFU_STM_CMD_READ_UNPROTECT		0x92
 | |
| 
 | |
| static gboolean
 | |
| dfu_target_stm_attach (DfuTarget *target, GError **error)
 | |
| {
 | |
| 	g_autoptr(GBytes) bytes_tmp = g_bytes_new (NULL, 0);
 | |
| 	return dfu_target_download_chunk (target, 2, bytes_tmp, error);
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| dfu_target_stm_mass_erase (DfuTarget *target, GError **error)
 | |
| {
 | |
| 	GBytes *data_in;
 | |
| 	guint8 buf[1];
 | |
| 
 | |
| 	/* format buffer */
 | |
| 	buf[0] = DFU_STM_CMD_ERASE;
 | |
| 	data_in = g_bytes_new_static (buf, sizeof(buf));
 | |
| 	if (!dfu_target_download_chunk (target, 0, data_in, error)) {
 | |
| 		g_prefix_error (error, "cannot mass-erase: ");
 | |
| 		return FALSE;
 | |
| 	}
 | |
| 
 | |
| 	/* 2nd check required to get error code */
 | |
| 	return dfu_target_check_status (target, error);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * dfu_target_stm_set_address:
 | |
|  * @target: a #DfuTarget
 | |
|  * @address: memory address
 | |
|  * @error: a #GError, or %NULL
 | |
|  *
 | |
|  * Sets the address used for the next download or upload request.
 | |
|  *
 | |
|  * Return value: %TRUE for success
 | |
|  **/
 | |
| static gboolean
 | |
| dfu_target_stm_set_address (DfuTarget *target, guint32 address, GError **error)
 | |
| {
 | |
| 	GBytes *data_in;
 | |
| 	guint8 buf[5];
 | |
| 
 | |
| 	/* format buffer */
 | |
| 	buf[0] = DFU_STM_CMD_SET_ADDRESS_POINTER;
 | |
| 	memcpy (buf + 1, &address, 4);
 | |
| 	data_in = g_bytes_new_static (buf, sizeof(buf));
 | |
| 	if (!dfu_target_download_chunk (target, 0, data_in, error)) {
 | |
| 		g_prefix_error (error, "cannot set address 0x%x: ", address);
 | |
| 		return FALSE;
 | |
| 	}
 | |
| 
 | |
| 	/* 2nd check required to get error code */
 | |
| 	g_debug ("doing actual check status");
 | |
| 	return dfu_target_check_status (target, error);
 | |
| }
 | |
| 
 | |
| static DfuElement *
 | |
| dfu_target_stm_upload_element (DfuTarget *target,
 | |
| 			       guint32 address,
 | |
| 			       gsize expected_size,
 | |
| 			       gsize maximum_size,
 | |
| 			       GError **error)
 | |
| {
 | |
| 	DfuDevice *device = dfu_target_get_device (target);
 | |
| 	DfuSector *sector;
 | |
| 	DfuElement *element = NULL;
 | |
| 	GBytes *chunk_tmp;
 | |
| 	guint32 offset = address;
 | |
| 	guint percentage_size = expected_size > 0 ? expected_size : maximum_size;
 | |
| 	gsize total_size = 0;
 | |
| 	guint16 transfer_size = dfu_device_get_transfer_size (device);
 | |
| 	g_autoptr(GBytes) contents = NULL;
 | |
| 	g_autoptr(GBytes) contents_truncated = NULL;
 | |
| 	g_autoptr(GPtrArray) chunks = NULL;
 | |
| 
 | |
| 	/* for DfuSe devices we need to handle the address manually */
 | |
| 	sector = dfu_target_get_sector_for_addr (target, offset);
 | |
| 	if (sector == NULL) {
 | |
| 		g_set_error (error,
 | |
| 			     FWUPD_ERROR,
 | |
| 			     FWUPD_ERROR_NOT_SUPPORTED,
 | |
| 			     "no memory sector at 0x%04x",
 | |
| 			     (guint) offset);
 | |
| 		return NULL;
 | |
| 	}
 | |
| 	g_debug ("using sector %u for read of %x",
 | |
| 		 dfu_sector_get_id (sector),
 | |
| 		 offset);
 | |
| 	if (!dfu_sector_has_cap (sector, DFU_SECTOR_CAP_READABLE)) {
 | |
| 		g_set_error (error,
 | |
| 			     FWUPD_ERROR,
 | |
| 			     FWUPD_ERROR_NOT_SUPPORTED,
 | |
| 			     "memory sector at 0x%04x is not readable",
 | |
| 			     (guint) offset);
 | |
| 		return NULL;
 | |
| 	}
 | |
| 
 | |
| 	/* update UI */
 | |
| 	dfu_target_set_action (target, FWUPD_STATUS_DEVICE_READ);
 | |
| 
 | |
| 	/* manually set the sector address */
 | |
| 	g_debug ("setting DfuSe address to 0x%04x", (guint) offset);
 | |
| 	if (!dfu_target_stm_set_address (target, offset, error))
 | |
| 		return NULL;
 | |
| 
 | |
| 	/* abort back to IDLE */
 | |
| 	if (!dfu_device_abort (device, error))
 | |
| 		return NULL;
 | |
| 
 | |
| 	/* get all the chunks from the hardware */
 | |
| 	chunks = g_ptr_array_new_with_free_func ((GDestroyNotify) g_bytes_unref);
 | |
| 	for (guint16 idx = 0; idx < G_MAXUINT16; idx++) {
 | |
| 		guint32 chunk_size;
 | |
| 
 | |
| 		/* read chunk of data -- ST uses wBlockNum=0 for DfuSe commands
 | |
| 		 * and wBlockNum=1 is reserved */
 | |
| 		chunk_tmp = dfu_target_upload_chunk (target,
 | |
| 						     idx + 2,
 | |
| 						     0, /* device transfer size */
 | |
| 						     error);
 | |
| 		if (chunk_tmp == NULL)
 | |
| 			return NULL;
 | |
| 
 | |
| 		/* add to array */
 | |
| 		chunk_size = (guint32) g_bytes_get_size (chunk_tmp);
 | |
| 		g_debug ("got #%04x chunk @0x%x of size %" G_GUINT32_FORMAT,
 | |
| 			 idx, offset, chunk_size);
 | |
| 		g_ptr_array_add (chunks, chunk_tmp);
 | |
| 		total_size += chunk_size;
 | |
| 		offset += chunk_size;
 | |
| 
 | |
| 		/* update UI */
 | |
| 		if (chunk_size > 0)
 | |
| 			dfu_target_set_percentage (target, total_size, percentage_size);
 | |
| 
 | |
| 		/* detect short write as EOF */
 | |
| 		if (chunk_size < transfer_size)
 | |
| 			break;
 | |
| 
 | |
| 		/* more data than we needed */
 | |
| 		if (maximum_size > 0 && total_size > maximum_size)
 | |
| 			break;
 | |
| 	}
 | |
| 
 | |
| 	/* abort back to IDLE */
 | |
| 	if (!dfu_device_abort (device, error))
 | |
| 		return NULL;
 | |
| 
 | |
| 	/* check final size */
 | |
| 	if (expected_size > 0) {
 | |
| 		if (total_size < expected_size) {
 | |
| 			g_set_error (error,
 | |
| 				     FWUPD_ERROR,
 | |
| 				     FWUPD_ERROR_INVALID_FILE,
 | |
| 				     "invalid size, got %" G_GSIZE_FORMAT ", "
 | |
| 				     "expected %" G_GSIZE_FORMAT ,
 | |
| 				     total_size, expected_size);
 | |
| 			return NULL;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/* done */
 | |
| 	dfu_target_set_percentage_raw (target, 100);
 | |
| 	dfu_target_set_action (target, FWUPD_STATUS_IDLE);
 | |
| 
 | |
| 	/* create new image */
 | |
| 	contents = dfu_utils_bytes_join_array (chunks);
 | |
| 	if (expected_size > 0)
 | |
| 		contents_truncated = g_bytes_new_from_bytes (contents, 0, expected_size);
 | |
| 	else
 | |
| 		contents_truncated = g_bytes_ref (contents);
 | |
| 	element = dfu_element_new ();
 | |
| 	dfu_element_set_contents (element, contents_truncated);
 | |
| 	dfu_element_set_address (element, address);
 | |
| 	return element;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * dfu_target_stm_erase_address:
 | |
|  * @target: a #DfuTarget
 | |
|  * @address: memory address
 | |
|  * @error: a #GError, or %NULL
 | |
|  *
 | |
|  * Erases a memory sector at a given address.
 | |
|  *
 | |
|  * Return value: %TRUE for success
 | |
|  **/
 | |
| static gboolean
 | |
| dfu_target_stm_erase_address (DfuTarget *target, guint32 address, GError **error)
 | |
| {
 | |
| 	GBytes *data_in;
 | |
| 	guint8 buf[5];
 | |
| 
 | |
| 	/* format buffer */
 | |
| 	buf[0] = DFU_STM_CMD_ERASE;
 | |
| 	memcpy (buf + 1, &address, 4);
 | |
| 	data_in = g_bytes_new_static (buf, sizeof(buf));
 | |
| 	if (!dfu_target_download_chunk (target, 0, data_in, error)) {
 | |
| 		g_prefix_error (error, "cannot erase address 0x%x: ", address);
 | |
| 		return FALSE;
 | |
| 	}
 | |
| 
 | |
| 	/* 2nd check required to get error code */
 | |
| 	g_debug ("doing actual check status");
 | |
| 	return dfu_target_check_status (target, error);
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| dfu_target_stm_download_element (DfuTarget *target,
 | |
| 				 DfuElement *element,
 | |
| 				 DfuTargetTransferFlags flags,
 | |
| 				 GError **error)
 | |
| {
 | |
| 	DfuDevice *device = dfu_target_get_device (target);
 | |
| 	DfuSector *sector;
 | |
| 	GBytes *bytes;
 | |
| 	guint nr_chunks;
 | |
| 	guint zone_last = G_MAXUINT;
 | |
| 	guint16 transfer_size = dfu_device_get_transfer_size (device);
 | |
| 	g_autoptr(GPtrArray) sectors_array = NULL;
 | |
| 	g_autoptr(GHashTable) sectors_hash = NULL;
 | |
| 
 | |
| 	/* round up as we have to transfer incomplete blocks */
 | |
| 	bytes = dfu_element_get_contents (element);
 | |
| 	nr_chunks = (guint) ceil ((gdouble) g_bytes_get_size (bytes) /
 | |
| 				  (gdouble) transfer_size);
 | |
| 	if (nr_chunks == 0) {
 | |
| 		g_set_error_literal (error,
 | |
| 				     FWUPD_ERROR,
 | |
| 				     FWUPD_ERROR_INVALID_FILE,
 | |
| 				     "zero-length firmware");
 | |
| 		return FALSE;
 | |
| 	}
 | |
| 
 | |
| 	/* 1st pass: work out which sectors need erasing */
 | |
| 	sectors_array = g_ptr_array_new ();
 | |
| 	sectors_hash = g_hash_table_new (g_direct_hash, g_direct_equal);
 | |
| 	for (guint i = 0; i < nr_chunks; i++) {
 | |
| 		guint32 offset_dev;
 | |
| 
 | |
| 		/* for DfuSe devices we need to handle the erase and setting
 | |
| 		 * the sectory address manually */
 | |
| 		offset_dev = dfu_element_get_address (element) + (i * transfer_size);
 | |
| 		sector = dfu_target_get_sector_for_addr (target, offset_dev);
 | |
| 		if (sector == NULL) {
 | |
| 			g_set_error (error,
 | |
| 				     FWUPD_ERROR,
 | |
| 				     FWUPD_ERROR_NOT_SUPPORTED,
 | |
| 				     "no memory sector at 0x%04x",
 | |
| 				     (guint) offset_dev);
 | |
| 			return FALSE;
 | |
| 		}
 | |
| 		if (!dfu_sector_has_cap (sector, DFU_SECTOR_CAP_WRITEABLE)) {
 | |
| 			g_set_error (error,
 | |
| 				     FWUPD_ERROR,
 | |
| 				     FWUPD_ERROR_NOT_SUPPORTED,
 | |
| 				     "memory sector at 0x%04x is not writable",
 | |
| 				     (guint) offset_dev);
 | |
| 			return FALSE;
 | |
| 		}
 | |
| 
 | |
| 		/* if it's erasable and not yet blanked */
 | |
| 		if (dfu_sector_has_cap (sector, DFU_SECTOR_CAP_ERASEABLE) &&
 | |
| 		    g_hash_table_lookup (sectors_hash, sector) == NULL) {
 | |
| 			g_hash_table_insert (sectors_hash,
 | |
| 					     sector,
 | |
| 					     GINT_TO_POINTER (1));
 | |
| 			g_ptr_array_add (sectors_array, sector);
 | |
| 			g_debug ("marking sector 0x%04x-%04x to be erased",
 | |
| 				 dfu_sector_get_address (sector),
 | |
| 				 dfu_sector_get_address (sector) + dfu_sector_get_size (sector));
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/* 2nd pass: actually erase sectors */
 | |
| 	dfu_target_set_action (target, FWUPD_STATUS_DEVICE_ERASE);
 | |
| 	for (guint i = 0; i < sectors_array->len; i++) {
 | |
| 		sector = g_ptr_array_index (sectors_array, i);
 | |
| 		g_debug ("erasing sector at 0x%04x",
 | |
| 			 dfu_sector_get_address (sector));
 | |
| 		if (!dfu_target_stm_erase_address (target,
 | |
| 						   dfu_sector_get_address (sector),
 | |
| 						   error))
 | |
| 			return FALSE;
 | |
| 		dfu_target_set_percentage (target, i + 1, sectors_array->len);
 | |
| 	}
 | |
| 	dfu_target_set_percentage_raw (target, 100);
 | |
| 	dfu_target_set_action (target, FWUPD_STATUS_IDLE);
 | |
| 
 | |
| 	/* 3rd pass: write data */
 | |
| 	dfu_target_set_action (target, FWUPD_STATUS_DEVICE_WRITE);
 | |
| 	for (guint i = 0; i < nr_chunks; i++) {
 | |
| 		gsize length;
 | |
| 		guint32 offset;
 | |
| 		guint32 offset_dev;
 | |
| 		g_autoptr(GBytes) bytes_tmp = NULL;
 | |
| 
 | |
| 		/* caclulate the offset into the element data */
 | |
| 		offset = i * transfer_size;
 | |
| 		offset_dev = dfu_element_get_address (element) + offset;
 | |
| 
 | |
| 		/* for DfuSe devices we need to set the address manually */
 | |
| 		sector = dfu_target_get_sector_for_addr (target, offset_dev);
 | |
| 		g_assert (sector != NULL);
 | |
| 
 | |
| 		/* manually set the sector address */
 | |
| 		if (dfu_sector_get_zone (sector) != zone_last) {
 | |
| 			g_debug ("setting address to 0x%04x",
 | |
| 				 (guint) offset_dev);
 | |
| 			if (!dfu_target_stm_set_address (target,
 | |
| 							 (guint32) offset_dev,
 | |
| 							 error))
 | |
| 				return FALSE;
 | |
| 			zone_last = dfu_sector_get_zone (sector);
 | |
| 		}
 | |
| 
 | |
| 		/* we have to write one final zero-sized chunk for EOF */
 | |
| 		length = g_bytes_get_size (bytes) - offset;
 | |
| 		if (length > transfer_size)
 | |
| 			length = transfer_size;
 | |
| 		bytes_tmp = g_bytes_new_from_bytes (bytes, offset, length);
 | |
| 		g_debug ("writing sector at 0x%04x (0x%" G_GSIZE_FORMAT ")",
 | |
| 			 offset_dev,
 | |
| 			 g_bytes_get_size (bytes_tmp));
 | |
| 		/* ST uses wBlockNum=0 for DfuSe commands and wBlockNum=1 is reserved */
 | |
| 		if (!dfu_target_download_chunk (target,
 | |
| 						(guint8) (i + 2),
 | |
| 						bytes_tmp,
 | |
| 						error))
 | |
| 			return FALSE;
 | |
| 
 | |
| 		/* getting the status moves the state machine to DNLOAD-IDLE */
 | |
| 		if (!dfu_target_check_status (target, error))
 | |
| 			return FALSE;
 | |
| 
 | |
| 		/* update UI */
 | |
| 		dfu_target_set_percentage (target, offset, g_bytes_get_size (bytes));
 | |
| 	}
 | |
| 
 | |
| 	/* done */
 | |
| 	dfu_target_set_percentage_raw (target, 100);
 | |
| 	dfu_target_set_action (target, FWUPD_STATUS_IDLE);
 | |
| 
 | |
| 	/* success */
 | |
| 	return TRUE;
 | |
| }
 | |
| 
 | |
| static void
 | |
| dfu_target_stm_init (DfuTargetStm *target_stm)
 | |
| {
 | |
| }
 | |
| 
 | |
| static void
 | |
| dfu_target_stm_class_init (DfuTargetStmClass *klass)
 | |
| {
 | |
| 	DfuTargetClass *klass_target = DFU_TARGET_CLASS (klass);
 | |
| 	klass_target->attach = dfu_target_stm_attach;
 | |
| 	klass_target->mass_erase = dfu_target_stm_mass_erase;
 | |
| 	klass_target->upload_element = dfu_target_stm_upload_element;
 | |
| 	klass_target->download_element = dfu_target_stm_download_element;
 | |
| }
 | |
| 
 | |
| DfuTarget *
 | |
| dfu_target_stm_new (void)
 | |
| {
 | |
| 	DfuTarget *target;
 | |
| 	target = g_object_new (DFU_TYPE_TARGET_STM, NULL);
 | |
| 	return target;
 | |
| }
 | 
