Simulating embedded systems response, similar to tht of an MCU

 A feedback controller in most cases would be implemented digitally inside a micro-controller. In a lot of cases it can be hard to take into account the effects of ADC sampling and delays caused by the processing inside the micro-controller. 

In this blog  I demonstrate how these effects can be effectively simulated. I take the example of ATmega328p (the MCU present in Arduino uno) and simulate the response when it is using ADC readings to drive a PWM output(ADC reading determines the duty cycle o the PWM). I will not be simulating all the registers and load a binary (something which soft-wares like micro-chip studio do). I will be creating a simulation which gives the response similar to the one one would have when implemented in the MCU). 

I will be using ngspice as the software for simulations and an Xspice code model will be created for the purpose of simulation. 

First I show the interface specification file of the model.


NAME_TABLE :
C_Function_name : c_atmega_adc_pwm
Spice_Model_Name : atmega_adc_pwm
Description : " driving apwm of atmega based on adc reading"


PORT_TABLE :
PORT_Name : clock adc_in
Description : "clock pin" "adc input pin"
Direction : in in
Default_type : v v
Allowed_Types : [v] [v]
Vector : no no
Vector_Bounds : - -
Null_Allowed : no no

PORT_TABLE :
PORT_Name : pwm
Description : "pwm_output pin"
Direction : out
Default_type : v
Allowed_Types : [v]
Vector : no
Vector_Bounds : -
Null_Allowed : no 

The clock pin is the driving clock of the controller adc_in is the adc input pin and pwm is the output pin which drives the pwm output.

The functioning of the model is defined in the cfunc.mod file. 

Model definition

 Every MCU instruction is executed with a clock cycle. In this section I describe how is the clock implemented for the model. From a circuit point off view, clock is given as a square wave (PULSE in ngspice). I order to detect an edge of the clock, a differentiator is implemented. If the output of the differentiator crosses a certain peak then that means an edge has been detected. 

the exact c code is : 

void c_atmega_adc_pwm(ARGS){
 
static double clock_v, clock_v_cache, time_pre, edge, adc_v;
if(INIT){
printf("\ncreating log file");
printf("\ncreating log file");
pwm_log = fopen("/home/harsh/pwmlogs.csv", "w+");
}
clock_v = INPUT(clock);
adc_v = INPUT(adc_in);

if(TIME - time_pre > 0.001){ //if implemented in each iteration 
                                //this might cause convergence issues. 
                                //not sure why this happens but was 
                                //observing this  
edge = (clock_v - clock_v_cache) / (TIME - time_pre);
clock_v_cache = clock_v;
time_pre = TIME;
if(edge > 1000){ //the actual value of edge would be dependent on
                           //clock voltage and the time step value
            //write the code here 
            
}} 

 The last if statement would return true only when there is an edge of the clock.

MCU registers

The micro-controller is modeled as a separate structure :- 

typedef struct _atmega_struct{
uint8_t gp_register;
uint8_t ADCL;
uint8_t TCNT;
uint8_t OCR0A;
uint8_t PC;
}atmega_struct;

The ADCL is the data register of ADC where the digitized value is stored.  PC is the program counter. TCNT the timer counter for pwm, gp_register is a general purpose register. The MCU is initialized as a static variable :

static atmega_struct mcu = {
.PC = 0,
.ADCL = 0,
.gp_register = 0,
.OCR0A = 0,
.TCNT = 0
};

ADC

In this section I will describe the ADC of the model. For this example I am assuming that the ADC of ATmega is in "free running" mode where it takes 13 clock cycles for a single conversion. (for more details refer to chapter 23 of ATmega328p datasheet). In this mode the next conversion on the next clock edge itself. There is no need of external edge or changing the ADSC bit. 

ADC of Atmega in free running mode (taken from datasheet)
For now we assume that the voltages applied can be fully handled by the 8 bit ADCL register. (ADCH value is zero at all times). The ADCL register will be updated after \ every 13 cycles. the register can simply be modeled using an integer of type uint8_t (8 bit unsigned integer). 

In the code this would look like : 


if(adc_counter == 0){
mcu.ADCL = a2d;
a2d = (adc_v / 10) * 0xFF;
}
else if(adc_counter == 13){
adc_counter = 0;
}
else{
adc_counter++;
}

mcu.ADCL is the data register, a2d is the value that will be stored on completion of adc conversion cycle. (note that the value captured /sampled now will be stored in register after 13 cycles). adc_counter updated on each clock edge to keep track of the adc clock cycles. 

 PWM model

For this example we take the pwm timer to be in fast pwm mode. Where the output is set (high) on compare match with a compare register and cleared on a timer reset (when it reaches 0xFF). (refer to chapter 14 of atmega datasheet). 

 

8 bit timer in fast pwm mode

here also the compare match register and the counter register can be modeled by 8 bit unsigned integers. This can simply be modeled in c code as follows :- 

if(mcu.TCNT != 0xFF){
mcu.TCNT++;
}
else{
mcu.TCNT = 0; //counter updated on each clock
pwm_o = 1;
}
if(mcu.TCNT == mcu.OCR0A){
pwm_o = 0;
}

pwm_o is the output bit of pwm. 

 Updating the compare register

This is the tricky part. We need to update the output compare register of pwm (output_comp) with the data register of adc (adc_data) in an infinite loop. In c code this would be as simple as writing : 

while(1){
OCR0A = ADCL;
}

However when the MCU actually executes this line of code  it will be executing three instruction :

loop:
IN R0, ADCL
OUT OCR0A, R0
JMP loop 

 These three instructions are modeled as separate functions.

void intructIn(atmega_struct * atmega){
atmega->gp_register = atmega->ADCL;
atmega->PC += 1;
}
void instructOUT(atmega_struct * atmega){
atmega->OCR0A = atmega->gp_register;
atmega->PC += 1;
}
void intructJMP(atmega_struct * atmega){
atmega->PC = 0;
}

As would be expected in a real MCU, after each instruction the program counter is updated. The jump instruction makes the jump to the start and R0 is modeled by the gp_register (general purpose register). The entire program is modeled as an array of these three functins :

static void (*action_ptr[])(atmega_struct *) = {intructIn, instructOUT, \
                                                intructJMP};

Each instruction will be executed in every clock cycle depending on the prog counter value.

action_ptr[mcu.PC](&mcu);

The instruction itself will update the prog. counter (mcu.PC) as in the instruction functions, and in the next clock cycle the corresponding instruction will be executed. 

The entire cfunc.mod file is given below : 

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>


FILE * pwm_log;

typedef struct _atmega_struct{
uint8_t gp_register;
uint8_t ADCL;
uint8_t TCNT;
uint8_t OCR0A;
uint8_t PC;
}atmega_struct;

void intructIn(atmega_struct * atmega){
atmega->gp_register = atmega->ADCL;
atmega->PC += 1;
}
void instructOUT(atmega_struct * atmega){
atmega->OCR0A = atmega->gp_register;
atmega->PC += 1;
}
void intructJMP(atmega_struct * atmega){
atmega->PC = 0;
}



void c_atmega_adc_pwm(ARGS){

static void (*action_ptr[])(atmega_struct *) = {intructIn, instructOUT, \
                    intructJMP};
static double clock_v, clock_v_cache, time_pre, edge, adc_v;
static uint8_t adc_counter,a2d;
static _Bool pwm_o = 0;
static atmega_struct mcu = {
.PC = 0,
.ADCL = 0,
.gp_register = 0,
.OCR0A = 0,
.TCNT = 0
};
if(INIT){
printf("\ncreating log file");
printf("\ncreating log file");
pwm_log = fopen("/home/harsh/tessim_app/pwmlogs.csv", "w+");
}
clock_v = INPUT(clock);
adc_v = INPUT(adc_in);

if(TIME - time_pre > 0.001){
edge = (clock_v - clock_v_cache) / (TIME - time_pre);
clock_v_cache = clock_v;
time_pre = TIME;
if(edge > 1000){
fprintf(pwm_log,"\ncounter : %X , edge : %f , mcu.OCR0A : %X time : \
                             %f , clock_volt : %f", mcu.TCNT, edge, mcu.OCR0A,\
                                TIME, clock_v);
if(mcu.TCNT != 0xFF){
mcu.TCNT++;
}
else{
mcu.TCNT = 0; //counter updated on each clock
pwm_o = 1;
}
if(mcu.TCNT == mcu.OCR0A){
pwm_o = 0;
}

if(adc_counter == 0){
mcu.ADCL = a2d;
a2d = (adc_v / 10) * 0xFF;
}
else if(adc_counter == 13){
adc_counter = 0;
}
else{
adc_counter++;
}

action_ptr[mcu.PC](&mcu);
}

}

OUTPUT(pwm) = pwm_o * 10;

}

 Results : 

Compiling the above xspice code model with ngspice source (with name atmega_adc_pwm) and running the following examle file :

*atmega example for driving pwm with ADC reference

.model control atmega_adc_pwm

v1 clock 0 PULSE(0 5 0 1ns 1ns 0.5 1 0)
v2 adc_in 0 dc 1

A1 clock adc_in pwm control

.control
tran 1ms 600
.endc

 we get the following reslt : 


Increasing the adc input voltage increases the pwm duty cycle. : 

for v= 5 for example : 


Conclusion: 

This simulation can take into account ADC sampling rate and delays due to processing within the processor. (for example in a buck converter the delays will effect the response time of the converter).

In this post I have demonstrated a method by which an MCU can be accurately simulated. This is a crude and quick fix method. A better way (which would also require some more work) would be to simulate all the registers and the entire memory of an MCU and "load" a binary and execute each instruction and update the corresponding registers (and pins).

 I am also  working on an entire model for an MCU. If you are also interested in simulations o embedded systems and they can aid in your project, do contact me:

mail : chittoraharsh98@gmail.com

fb : https://www.facebook.com/harsh.chittroa/

LinkedIn : https://www.linkedin.com/in/harsh-chittora-346540149/ 

Comments

Popular posts from this blog

Simulating a motor in ngspice

Counting pulses in Arduino (without interrupts or loops)

Embedded system simulations using tessim