Maybe i'm missing something here (paragraphs would help) but with the C18 compiler local variables within a function are generally allocated on the software stack so i have no idea what the following means:
but the reality is you have a finite
amount of space to allocate all local
function vars that exist in an entire
project
By moving all your variables to globals within a module you are requiring there is space for all of them at the same time.
buffers and have to access that memory
via pointers so I can avoid the
semantics of memory banking.
What compiler are you using?
You're asking a very broad question; there are a wide variety of PICs, and I would probably not use "formal" memory management for any of them. There is no need.
A/D conversion results, filtering, UART buffers, etc. would normally be stored in plain old arrays in memory. You may augment them with head and tail pointers for ring buffers or index variables or other means, but your typical data manipulation techniques are no different for a PIC than you would have on a PC once you've got the address for the buffer. With small embedded microcontrollers you simply declare the array and use it.
Alternatively, you can work with the linker to artificially lower the amount of available memory that the system will see and then use the table read/write instructions and/or standard pointer accesses to hit this "hidden" memory. You reduce the memory the compiler can use so that the stack (which typically grows from the top of memory down) does not collide with your storage area, or BSS and the heap (which are usually located at the start of data memory) don't interfere with your memory area. The exact means to achieve this linker magic depends on the compiler suite of course.
If you could provide some more details, perhaps more specific application as well I could provide more specific answers. I think you are perhaps over-analyzing the issue. C is C; you can declare arbitrary memory and use it on a PC as well, so long as you don't run afoul of the MMU.
edit to add example ADC reading and averaging code:
I tend to write my code simply and clearly; I use small single-purpose functions and tend to write code as state machines. I also tend to leave optimization to the compiler unless I have a specific need to hand-optimize. This has come from years (almost two decades) of embedded design and learning things the hard way.
The code sets up a 1ms timer interrupt, two ADC channels and then samples them continuously; the readings are filtered using a simple sliding average filter. In this example, the data samples from each ADC channel are not stored individually. Each channel has its own running sum; the last 8 values (the FILTER_POINTS constant) are averaged and this filtered count value is stored. Care must be taken to make sure that the sum variable is big enough to store the number that can be created if 8 ADC samples are full-scale.
Finally, a temperature in 1/10 of a degree F, using a conversion function. The conversion function of course is specific to the sensors that I'm sampling. Your own conversion function(s) would have to be written to suit your sensors.
This is written for use with the CCS compiler; I do NOT like this compiler for a variety of reasons, but it's what most of my clients use because it's price is the lowest among its competitors.
#device PIC24F32KA304 ADC=12
#device ANSI
#include <24f32ka304.h>
#fuses OSCIO, NOPROTECT, NOWDT
#use delay(OSC=8MHz, CLOCK=32MHz)
#use standard_io(all)
#include <stdlib.h>
#include <math.h>
#include <stdio.h>
/* I hate mixed case types */
typedef BOOLEAN bool;
/* pin definitions */
#define PIN_FOO (PIN_C8)
#define PIN_BAR (PIN_A4)
#define PIN_BAZ (PIN_A9)
#define VREFP (0)
#define VREFN (1)
#define CHAN1 (4)
#define CHAN2 (5)
#define FILTER_POINTS (8)
/*
* 10mV per degree F.
* 2.048V reference, 4096 counts, for 0.5mV per count
* therefore 10mV/0.5mV or 20 counts per degree F
*/
#define COUNTS_PER_DEGREE (20)
#define COUNTS_PER_DEG_TENTHS (COUNTS_PER_DEGREE / 10)
/* globals */
volatile bool one_ms_flag;
volatile bool onehundred_ms_flag;
volatile bool one_second_flag;
unsigned int32 chan1_sum, chan2_sum; /* summing values for the averaging filter */
unsigned int16 chan1_counts, chan2_counts; /* raw ADC values for temp sensors */
unsigned int16 chan1_temp, chan2_temp; /* temperatures, in 0.1 degrees F steps */
/*
* Timer ISR
*/
#INT_TIMER1 level=7
void system_isr(void)
{
static int ticks = 0;
one_ms_flag = TRUE;
if ((ticks % 100) == 0) {
onehundred_ms_flag = TRUE;
}
if (++ticks > 999) {
one_second_flag = TRUE;
ticks = 0;
}
}
/*
* stupid simple sliding average (FILTER_POINTS data points)
* runsum is the running average (which is basically the sum of the last FILTER_POINTS values)
* function returns the new average
*/
static unsigned int16 filter_analog(unsigned int32 *runsum, unsigned int16 new)
{
*runsum -= *runsum / FILTER_POINTS;
*runsum += new;
return *runsum / FILTER_POINTS;
}
/*
* counts are 0.5mV/count, and sensor is 10mV/oF
* returns in 1/10 degrees (60F = 600)
* rounds to half a degree
*/
int to_degrees_f(int counts)
{
int deg_f;
int tenths;
deg_f = counts / COUNTS_PER_DEG_TENTHS;
tenths = deg_f % 10;
deg_f /= 10;
deg_f *= 10;
if (tenths >= 5) {
deg_f += 5;
}
return deg_f;
}
/*
* ADC loop
* meant to be called every 100ms or so.
* if reset is true, will reset the ADC state machine and reconfigure the ADC.
*/
void adc_loop(bool reset)
{
static enum { ADC_INIT=0, SMPL_CHAN1, SMPL_CHAN2 } adc_state = ADC_INIT;
unsigned int16 adc_value;
if (reset) {
adc_state = ADC_INIT;
}
switch(adc_state) {
case ADC_INIT:
setup_adc(ADC_CLOCK_DIV_128);
setup_adc_ports(sAN0|sAN1, VREF_VREF);
set_adc_channel(CHAN1);
read_adc(ADC_START_ONLY);
chan1_sum = chan2_sum = FILTER_SEED;
chan1_counts = chan2_counts = 0;
chan1_temp = chan2_temp = 0;
adc_state = SMPL_CHAN1;
break;
case SMPL_CHAN1:
adc_value = read_adc(ADC_READ_ONLY);
chan1_counts = filter_analog(&chan1_sum, adc_value);
chan1_temp = to_degrees_f(chan1_counts);
set_adc_channel(CHAN2);
read_adc(ADC_START_ONLY);
adc_state = SMPL_CHAN2;
break;
case SMPL_CHAN2:
adc_value = read_adc(ADC_READ_ONLY);
chan2_counts = filter_analog(&chan2_sum, adc_value);
chan2_temp = to_degrees_f(chan2_counts);
set_adc_channel(CHAN1);
read_adc(ADC_START_ONLY);
adc_state = SMPL_CHAN1;
break;
default:
adc_state = ADC_INIT;
};
}
void main(void)
{
/* Set up timer1 for 1ms interrupts. Tcy = Fosc/2 = 16MHz, /64 is 4us. 250 4us ticks is 1ms. */
setup_timer1(TMR_INTERNAL | TMR_DIV_BY_64, 250);
/* set up I/O */
output_low(PIN_FOO);
output_low(PIN_BAR);
output_float(PIN_BAZ);
/* reset the ADC subsystem */
adc_loop(TRUE);
/* variable setup */
one_second_flag = onehundred_ms_flag = one_ms_flag = FALSE;
enable_interrupts(INT_TIMER1);
/* main loop */
while (1) {
if (one_ms_flag) {
/* do stuff every 1ms */
one_ms_flag = FALSE;
}
if (onehundred_ms_flag) {
adc_loop(FALSE);
onehundred_ms_flag = FALSE;
}
if (one_second_flag) {
/* do stuff every 1s */
one_second_flag = FALSE;
}
}
}
Best Answer
At RESET, the MMU is usually 'off', i.e. it doesn't do any translation of memory addresses. So the RESET vector is retrieved from BIOS EPROM/Flash because the BIOS memory can actually be at that physical address.
The CPU will eventually switch the MMU on, when enough of the MMU's data has been set up. For example the MMU may have tables, in memory arrays, which will convert a virtual address to a physical address.
There are several types of MMU. The simplest have a base address which is an offset into physical memory, and a limit, which is the largest address that a program can access. This type is common in 32bit MCUs.
More complex MMU's will create the illusion of virtual addressing. A programs address space appears to be contiguous for program and data, but actually isn't, and some of it may be stored on an external storage system like disk or SSD.
The MMU is 'on-chip' in modern CPU's. They were separate off-chip units for some CPU's in the 70's and part of the 80's.
AN MMU has a very 'intimate' relationship with the CPU. Every access to memory goes via the MMU. The MMU can prevent any instruction completing an access to memory. For example a load or store instruction, transferring data between the CPU and memory, might have to be prevented from completing because it is using an address which is not valid. So the MMU needs a fast, synchronised integration with the CPU.
For a CPU capable of virtual addressing, the MMU can prevent an instruction completing for a 'missing' virtual memory page by interrupting the CPU in mid-instruction. That CPU state is stored, and the process to retrieve the missing page from external storage started. Other programs may be run in the meantime. Eventually the page is loaded into memory, the page 'fixed up' in the MMU tables, so that it is valid, and then the incomplete instruction rerun to completion, possibly millions of instructions later.
There is a choice about the relationship between cache memory and the MMU. A cache might be indexed by physical memory addresses, or virtual memory addresses. If the cache is indexed by virtual addresses, lookup in cache may happen in parallel with the MMU. If the cache is indexed by physical addresses, lookup happens after the MMU has translated a virtual address to a physical address.
An MMU could not map video memory into the CPU's address space unless the video card already makes the video memory look like normal memory. The MMU doesn't perform any magic, it does address translation between the CPU and memory access. The MMU is set up for each logical (OS) process, e.g. separate application. The MMU can 'hide' parts of the address space from a process, and normally does. So the MMU could prevent a process accessing video memory if it is part of the CPU's underlying address space.
It is the MMU which allows an OS to create a virtual memory environment for processes. The OS is mostly constrained to use whatever virtual address architecture the MMU provides. The OS and MMU don't have to be exactly aligned, for example OS could use bigger pages than the MMU supports, but it would be extremely awkward for the OS to try to use smaller pages.
It is the MMU which enables the OS to provide the illusion of virtual memory. Without a suitable MMU, it would be impractical to provide virtual memory.
(Without a virtual memory capable MMU, the illusion of virtual memory, containing on process, would be managed by an interpreter like the Java Virtual Machine (JVM), or the .NET runtime. They act upon every instruction to ensure the code of the Java/C# program can't damage anther program being run in the same address space.)
Typically the CPU runs in a couple of different 'modes', and the MMU wil treat an address differently depending on the mode. The OS will run in a more privileged 'mode' than a user process.
An MMU which supports virtual addressing will intercept every user-process access to memory (for both instructions and data) and convert the virtual address to a real, physical address. This translation doesn't necessarily happen. The MMU's translation tables also carry control bits, which the MMU enforces, which allow the user process permission to read, execute, or write to the physical address.