blob: 8dd1c0adb01e4d09182c7c7a04e0c32591959613 [file] [log] [blame]
/****************************************************************************
* arch/xtensa/src/esp32/esp32_himem.c
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership. The
* ASF licenses this file to you 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
*
* http://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.
*
****************************************************************************/
/****************************************************************************
* Included Files
****************************************************************************/
#include <stdlib.h>
#include <assert.h>
#include <debug.h>
#include <nuttx/kmalloc.h>
#include <nuttx/himem/himem.h>
#include <nuttx/spinlock.h>
#include "esp32_spiram.h"
#include "esp32_himem.h"
#include "esp32_spiflash.h"
#include "hardware/esp32_soc.h"
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
/* So, why does the API look this way and is so inflexible to not allow any
* maps beyond the full 32K chunks? Most of it has to do with the fact that
* the cache works on the *virtual* addresses What this comes down to is that
* while it's allowed to map a range of physical memory into the address
* space two times, there's no cache consistency between the two regions.
*
* This means that a write to region A may or may not show up, perhaps
* delayed, in region B, as it depends on the time that the writeback to SPI
* RAM is done on A and the time before the corresponding cache line is
* invalidated on B. Note that this goes for every 32-byte cache line: this
* implies that if a program writes to address X and Y within A, the write to
* Y may show up before the write to X does.
*
* It gets even worse when both A and B are written: theoretically, a write
* to a 32-byte cache line in A can be entirely undone because of a write to
* a different address in B that happens to be in the same 32-byte cache
* line.
*
* Because of these reasons, we do not allow double mappings at all. This,
* however, has other implications that make supporting ranges not really
* useful. Because the lack of double mappings, applications will need to do
* their own management of mapped regions, meaning they will normally map in
* and out blocks at a time anyway, as mapping more fluent regions would
* result in the chance of accidentally mapping two overlapping regions. As
* this is the case, to keep the code simple, at the moment we just force
* these blocks to be equal to the 32K MMU page size. The API itself does
* allow for more granular allocations, so if there's a pressing need for a
* more complex solution in the future, we can do this.
*
* Note: In the future, we can expand on this api to do a memcpy() between
* SPI RAM and (internal) memory using the SPI1 peripheral. This needs
* support for SPI1 to be in the SPI driver, however.
*/
/* How many 32KB pages will be reserved for bank switch */
#ifdef CONFIG_ESP32_SPIRAM_BANKSWITCH_ENABLE
# define SPIRAM_BANKSWITCH_RESERVE CONFIG_SPIRAM_BANKSWITCH_RESERVE
#else
# define SPIRAM_BANKSWITCH_RESERVE 0
#endif
/* Start of the virtual address range reserved for himem use */
#define VIRT_HIMEM_RANGE_START (SOC_EXTRAM_DATA_LOW + \
(128 - SPIRAM_BANKSWITCH_RESERVE) * \
CACHE_BLOCKSIZE)
/* Start MMU block reserved for himem use */
#define VIRT_HIMEM_RANGE_BLOCKSTART (128-SPIRAM_BANKSWITCH_RESERVE)
/* Start physical block */
#define PHYS_HIMEM_BLOCKSTART (128 - SPIRAM_BANKSWITCH_RESERVE)
#define HIMEM_CHECK(cond, str, err) if (cond) \
do \
{ merr("%s: %s", __FUNCTION__, str); \
return err; \
} while(0)
/* Character driver methods */
static ssize_t himem_read(struct file *filep, char *buffer,
size_t buflen);
static ssize_t himem_write(struct file *filep, const char *buffer,
size_t buflen);
static int himem_ioctl(struct file *filep, int cmd,
unsigned long arg);
/* Metadata for a block of physical RAM */
typedef struct
{
unsigned int is_alloced: 1;
unsigned int is_mapped: 1;
} ramblock_t;
/* Metadata for a 32-K memory address range */
typedef struct
{
unsigned int is_alloced: 1;
unsigned int is_mapped: 1;
unsigned int ram_block: 16;
} rangeblock_t;
static ramblock_t *g_ram_descriptor = NULL;
static rangeblock_t *g_range_descriptor = NULL;
static int g_ramblockcnt = 0;
static const int g_rangeblockcnt = SPIRAM_BANKSWITCH_RESERVE;
/* Used by the spinlock */
irqstate_t spinlock_flags;
/****************************************************************************
* Private Data
****************************************************************************/
static const struct file_operations g_himemfops =
{
NULL, /* open */
NULL, /* close */
himem_read, /* read */
himem_write, /* write */
NULL, /* seek */
himem_ioctl, /* ioctl */
};
/****************************************************************************
* Private functions
****************************************************************************/
static inline int ramblock_idx_valid(int ramblock_idx)
{
return (ramblock_idx >= 0 && ramblock_idx < g_ramblockcnt);
}
static inline int rangeblock_idx_valid(int rangeblock_idx)
{
return (rangeblock_idx >= 0 && rangeblock_idx < g_rangeblockcnt);
}
/****************************************************************************
* Public Functions
****************************************************************************/
size_t esp_himem_get_phys_size(void)
{
int paddr_start = (4096 * 1024) - (CACHE_BLOCKSIZE *
SPIRAM_BANKSWITCH_RESERVE);
return esp_spiram_get_size() - paddr_start;
}
size_t esp_himem_get_free_size(void)
{
size_t ret = 0;
int i;
for (i = 0; i < g_ramblockcnt; i++)
{
if (!g_ram_descriptor[i].is_alloced)
{
ret += CACHE_BLOCKSIZE;
}
}
return ret;
}
size_t esp_himem_reserved_area_size(void)
{
return CACHE_BLOCKSIZE * SPIRAM_BANKSWITCH_RESERVE;
}
int esp_himem_init(void)
{
int paddr_start = (4096 * 1024) - (CACHE_BLOCKSIZE *
SPIRAM_BANKSWITCH_RESERVE);
int paddr_end;
int maxram;
int ret;
if (SPIRAM_BANKSWITCH_RESERVE == 0)
{
return -ENODEV;
}
maxram = esp_spiram_get_size();
/* Catch double init */
/* Looks weird; last arg is empty so it expands to 'return ;' */
HIMEM_CHECK(g_ram_descriptor != NULL, "already initialized", 0);
HIMEM_CHECK(g_range_descriptor != NULL, "already initialized", 0);
/* need to have some reserved banks */
HIMEM_CHECK(SPIRAM_BANKSWITCH_RESERVE == 0, "No banks reserved for \
himem", 0);
/* Start and end of physical reserved memory. Note it starts slightly under
* the 4MiB mark as the reserved banks can't have an unity mapping to be
* used by malloc anymore; we treat them as himem instead.
*/
paddr_end = maxram;
g_ramblockcnt = ((paddr_end - paddr_start) / CACHE_BLOCKSIZE);
/* Allocate data structures */
g_ram_descriptor = kmm_zalloc(sizeof(ramblock_t) * g_ramblockcnt);
g_range_descriptor = kmm_zalloc(sizeof(rangeblock_t) * \
SPIRAM_BANKSWITCH_RESERVE);
if (g_ram_descriptor == NULL || g_range_descriptor == NULL)
{
merr("Cannot allocate memory for meta info. Not initializing!");
kmm_free(g_ram_descriptor);
kmm_free(g_range_descriptor);
return -ENOMEM;
}
/* Register the character driver */
ret = register_driver("/dev/himem", &g_himemfops, 0666, NULL);
if (ret < 0)
{
merr("ERROR: Failed to register driver: %d\n", ret);
}
minfo("Initialized. Using last %d 32KB address blocks for bank \
switching on %d KB of physical memory.\n",
SPIRAM_BANKSWITCH_RESERVE, (paddr_end - paddr_start) / 1024);
return OK;
}
/* Allocate count not-necessarily consecutive physical RAM blocks, return
* numbers in blocks[]. Return true if blocks can be allocated, false if not.
*/
static bool allocate_blocks(int count, uint16_t *blocks_out)
{
int n = 0;
int i;
for (i = 0; i < g_ramblockcnt && n != count; i++)
{
if (!g_ram_descriptor[i].is_alloced)
{
blocks_out[n] = i;
n++;
}
}
if (n == count)
{
/* All blocks could be allocated. Mark as in use. */
for (i = 0; i < count; i++)
{
g_ram_descriptor[blocks_out[i]].is_alloced = true;
DEBUGASSERT(g_ram_descriptor[blocks_out[i]].is_mapped == false);
}
return true;
}
else
{
/* Error allocating blocks */
return false;
}
}
int esp_himem_alloc(size_t size, esp_himem_handle_t *handle_out)
{
esp_himem_ramdata_t *r;
int blocks;
int ok;
/* If the size is not multiple of BLOCKSIZE, there is an issue */
if (size % CACHE_BLOCKSIZE != 0)
{
return -EINVAL;
}
blocks = size / CACHE_BLOCKSIZE;
r = kmm_malloc(sizeof(esp_himem_ramdata_t));
if (!r)
{
goto nomem;
}
r->block = kmm_malloc(sizeof(uint16_t) * blocks);
if (!r->block)
{
goto nomem;
}
spinlock_flags = spin_lock_irqsave(NULL);
ok = allocate_blocks(blocks, r->block);
spin_unlock_irqrestore(NULL, spinlock_flags);
if (!ok)
{
goto nomem;
}
r->block_ct = blocks;
*handle_out = r;
return OK;
nomem:
if (r)
{
kmm_free(r->block);
kmm_free(r);
}
return -ENOMEM;
}
int esp_himem_free(esp_himem_handle_t handle)
{
int i;
/* Check if any of the blocks is still mapped; fail if this is the case. */
for (i = 0; i < handle->block_ct; i++)
{
DEBUGASSERT(ramblock_idx_valid(handle->block[i]));
HIMEM_CHECK(g_ram_descriptor[handle->block[i]].is_mapped,
"block in range still mapped", -EINVAL);
}
/* Mark blocks as free */
spinlock_flags = spin_lock_irqsave(NULL);
for (i = 0; i < handle->block_ct; i++)
{
g_ram_descriptor[handle->block[i]].is_alloced = false;
}
spin_unlock_irqrestore(NULL, spinlock_flags);
/* Free handle */
kmm_free(handle->block);
kmm_free(handle);
return OK;
}
int esp_himem_alloc_map_range(size_t size,
esp_himem_rangehandle_t *handle_out)
{
esp_himem_rangedata_t *r;
int i;
int blocks;
int start_free;
HIMEM_CHECK(g_ram_descriptor == NULL, "Himem not available!",
-EINVAL);
HIMEM_CHECK(size % CACHE_BLOCKSIZE != 0,
"requested size not aligned to blocksize",
-EINVAL);
blocks = size / CACHE_BLOCKSIZE;
r = kmm_malloc(sizeof(esp_himem_rangedata_t) * 1);
if (!r)
{
return -ENOMEM;
}
r->block_ct = blocks;
r->block_start = -1;
start_free = 0;
spinlock_flags = spin_lock_irqsave(NULL);
for (i = 0; i < g_rangeblockcnt; i++)
{
if (g_range_descriptor[i].is_alloced)
{
start_free = i + 1; /* optimistically assume next block is free... */
}
else
{
if (i - start_free == blocks - 1)
{
/* We found a span of blocks that's big enough to allocate
* the requested range in.
*/
r->block_start = start_free;
break;
}
}
}
if (r->block_start == -1)
{
/* Couldn't find enough free blocks */
kmm_free(r);
spin_unlock_irqrestore(NULL, spinlock_flags);
return -ENOMEM;
}
/* Range is found. Mark the blocks as in use. */
for (i = 0; i < blocks; i++)
{
g_range_descriptor[r->block_start + i].is_alloced = 1;
}
spin_unlock_irqrestore(NULL, spinlock_flags);
/* All done. */
*handle_out = r;
return OK;
}
int esp_himem_free_map_range(esp_himem_rangehandle_t handle)
{
int i;
/* Check if any of the blocks in the range have a mapping */
for (i = 0; i < handle->block_ct; i++)
{
DEBUGASSERT(rangeblock_idx_valid(handle->block_start + i));
/* should be allocated, if handle is valid */
DEBUGASSERT(g_range_descriptor[i + \
handle->block_start].is_alloced == 1);
HIMEM_CHECK(g_range_descriptor[i + handle->block_start].is_mapped,
"memory still mapped to range", -EINVAL);
}
/* We should be good to free this. Mark blocks as free. */
spinlock_flags = spin_lock_irqsave(NULL);
for (i = 0; i < handle->block_ct; i++)
{
g_range_descriptor[i + handle->block_start].is_alloced = 0;
}
spin_unlock_irqrestore(NULL, spinlock_flags);
kmm_free(handle);
return OK;
}
int esp_himem_map(esp_himem_handle_t handle,
esp_himem_rangehandle_t range,
size_t ram_offset,
size_t range_offset,
size_t len,
int flags,
void **out_ptr)
{
int i;
int ram_block = ram_offset / CACHE_BLOCKSIZE;
int range_block = range_offset / CACHE_BLOCKSIZE;
int blockcount = len / CACHE_BLOCKSIZE;
HIMEM_CHECK(g_ram_descriptor == NULL, "Himem not available!",
-EINVAL);
/* Offsets and length must be block-aligned */
HIMEM_CHECK(ram_offset % CACHE_BLOCKSIZE != 0,
"ram offset not aligned to blocksize", -EINVAL);
HIMEM_CHECK(range_offset % CACHE_BLOCKSIZE != 0,
"range not aligned to blocksize", -EINVAL);
HIMEM_CHECK(len % CACHE_BLOCKSIZE != 0,
"length not aligned to blocksize", -EINVAL);
/* ram and range should be within allocated range */
HIMEM_CHECK(ram_block + blockcount > handle->block_ct,
"args not in range of phys ram handle", -EINVAL);
HIMEM_CHECK(range_block + blockcount > range->block_ct,
"args not in range of range handle", -EINVAL);
/* Check if ram blocks aren't already mapped, and if memory range is
* unmapped.
*/
for (i = 0; i < blockcount; i++)
{
HIMEM_CHECK(g_ram_descriptor[handle->block[i + ram_block]].is_mapped,
"ram already mapped", -EINVAL);
HIMEM_CHECK(g_range_descriptor[range->block_start + i +
range_block].is_mapped, "range already mapped",
-EINVAL);
}
/* Map and mark as mapped */
spinlock_flags = spin_lock_irqsave(NULL);
for (i = 0; i < blockcount; i++)
{
DEBUGASSERT(ramblock_idx_valid(handle->block[i + ram_block]));
g_ram_descriptor[handle->block[i + ram_block]].is_mapped = 1;
g_range_descriptor[range->block_start + i + range_block].is_mapped = 1;
g_range_descriptor[range->block_start + i + range_block].ram_block =
handle->block[i + ram_block];
}
spin_unlock_irqrestore(NULL, spinlock_flags);
for (i = 0; i < blockcount; i++)
{
esp32_set_bank(VIRT_HIMEM_RANGE_BLOCKSTART + range->block_start + i +
range_block, handle->block[i + ram_block] +
PHYS_HIMEM_BLOCKSTART, 1);
}
/* Set out pointer */
*out_ptr = (void *)(VIRT_HIMEM_RANGE_START +
(range->block_start + range_offset) * CACHE_BLOCKSIZE);
return OK;
}
int esp_himem_unmap(esp_himem_rangehandle_t range, void *ptr,
size_t len)
{
/* Note: doesn't actually unmap, just clears cache and marks blocks as
* unmapped.
* Future optimization: could actually lazy-unmap here: essentially, do
* nothing and only clear the cache when we re-use the block for a
* different physical address.
*/
int range_offset = (uint32_t)ptr - VIRT_HIMEM_RANGE_START;
int range_block = (range_offset / CACHE_BLOCKSIZE) - range->block_start;
int blockcount = len / CACHE_BLOCKSIZE;
int i;
HIMEM_CHECK(range_offset % CACHE_BLOCKSIZE != 0,
"range offset not block-aligned", -EINVAL);
HIMEM_CHECK(len % CACHE_BLOCKSIZE != 0,
"map length not block-aligned", -EINVAL);
HIMEM_CHECK(range_block + blockcount > range->block_ct,
"range out of bounds for handle", -EINVAL);
spinlock_flags = spin_lock_irqsave(NULL);
for (i = 0; i < blockcount; i++)
{
int ramblock = g_range_descriptor[range->block_start + i +
range_block].ram_block;
DEBUGASSERT(ramblock_idx_valid(ramblock));
g_ram_descriptor[ramblock].is_mapped = 0;
g_range_descriptor[range->block_start + i + range_block].is_mapped = 0;
}
esp_spiram_writeback_cache();
spin_unlock_irqrestore(NULL, spinlock_flags);
return OK;
}
/****************************************************************************
* Name: himem_read
****************************************************************************/
static ssize_t himem_read(struct file *filep, char *buffer,
size_t buflen)
{
return -ENOSYS;
}
/****************************************************************************
* Name: himem_write
****************************************************************************/
static ssize_t himem_write(struct file *filep, const char *buffer,
size_t buflen)
{
return -ENOSYS;
}
/****************************************************************************
* Name: himem_ioctl
****************************************************************************/
static int himem_ioctl(struct file *filep, int cmd, unsigned long arg)
{
int ret = OK;
switch (cmd)
{
/* Allocate the physical RAM blocks */
case HIMEMIOC_ALLOC_BLOCKS:
{
struct esp_himem_par *param =
(struct esp_himem_par *)((uintptr_t)arg);
DEBUGASSERT(param != NULL);
/* Allocate the memory we're going to check. */
ret = esp_himem_alloc(param->memfree, &(param->handle));
if (ret < 0)
{
minfo("Error: esp_himem_alloc() failed!\n");
return ret;
}
}
break;
/* Free the physical RAM blocks */
case HIMEMIOC_FREE_BLOCKS:
{
struct esp_himem_par *param =
(struct esp_himem_par *)((uintptr_t)arg);
DEBUGASSERT(param != NULL);
ret = esp_himem_free(param->handle);
if (ret < 0)
{
minfo("Error: esp_himem_free() failed!\n");
return ret;
}
}
break;
/* Allocate the mapping range */
case HIMEMIOC_ALLOC_MAP_RANGE:
{
struct esp_himem_par *param =
(struct esp_himem_par *)((uintptr_t)arg);
DEBUGASSERT(param != NULL);
/* Allocate a block of address range */
ret = esp_himem_alloc_map_range(ESP_HIMEM_BLKSZ, &(param->range));
if (ret < 0)
{
minfo("Error: esp_himem_alloc_map_range() failed!\n");
return ret;
}
}
break;
/* Free the mapping range */
case HIMEMIOC_FREE_MAP_RANGE:
{
struct esp_himem_par *param =
(struct esp_himem_par *)((uintptr_t)arg);
DEBUGASSERT(param != NULL);
ret = esp_himem_free_map_range(param->range);
if (ret < 0)
{
minfo("Error: esp_himem_free_map_range() failed!\n");
return ret;
}
}
break;
/* Map the himem blocks */
case HIMEMIOC_MAP:
{
struct esp_himem_par *param =
(struct esp_himem_par *)((uintptr_t)arg);
DEBUGASSERT(param != NULL);
ret = esp_himem_map(param->handle,
param->range,
param->ram_offset,
param->range_offset,
param->len,
param->flags,
(void **) &(param->ptr));
if (ret < 0)
{
minfo("error: esp_himem_map() failed!\n");
return ret;
}
}
break;
/* Unmap the himem blocks */
case HIMEMIOC_UNMAP:
{
struct esp_himem_par *param =
(struct esp_himem_par *)((uintptr_t)arg);
DEBUGASSERT(param != NULL);
ret = esp_himem_unmap(param->range,
(void *) param->ptr,
param->len);
if (ret < 0)
{
minfo("error: esp_himem_unmap() failed!\n");
return ret;
}
}
break;
/* Get the physical external memory size */
case HIMEMIOC_GET_PHYS_SIZE:
{
struct esp_himem_par *param =
(struct esp_himem_par *)((uintptr_t)arg);
DEBUGASSERT(param != NULL);
param->memcnt = esp_himem_get_phys_size();
}
break;
/* Get the free memory size */
case HIMEMIOC_GET_FREE_SIZE:
{
struct esp_himem_par *param =
(struct esp_himem_par *)((uintptr_t)arg);
DEBUGASSERT(param != NULL);
param->memfree = esp_himem_get_free_size();
}
break;
default:
{
sninfo("Unrecognized cmd: %d\n", cmd);
ret = -ENOTTY;
}
break;
}
return ret;
}