Variables
RPicSim uses the RPicSim::Variable class to let you access simulated program variables stored in RAM, program memory, or EEPROM, as well as Special Function Registers, which can be useful for unit testing.
To access a variable, RPicSim needs to know the name it will be called in your Ruby code, what type of memory it is stored in, what data type it is (e.g. 16-bit unsigned integer), and its address in memory. This information is deduced in different ways for the different types of variables described below.
User-defined variables
For variables defined in your firmware, RPicSim can usually deduce the address by looking at the symbol table in your COF file, so you will not need to type the address. However, RPicSim cannot deduce the data type of a variable, so any variables used need to be explicitly defined in the simulation class using def_var. For example:
class MySim < RPicSim::Sim
#...
def_var :counter, :uint8
end
The first argument to def_var
specifies what to call the
variable in Ruby code. Using the example above, you could access the
variable object by passing :counter
as the argument to
RPicSim::Sim#var:
sim.var(:counter)
Each variable also has a method on the simulation object by the same name. This means that you can access the variable like this:
sim.counter
A shortcut is also available in RSpec thanks to RPicsim's
RSpec integration, so you can simply write
counter
in any of your RSpec examples:
it "drives the main output high" do
expect(counter.value).to eq 44
end
The second argument to def_var
specifies the data type of the
variable. This is required. For the full list of allowed types, see
RPicSim::Sim::ClassDefinitionMethods#def_var.
In the example above, RPicSim will look in your firmware's COF file for a RAM symbol named “counter” and it will use that as the address for the variable, so you do not need to specify the address yourself.
You can use the symbol
option to specify what symbol in the
symbol table marks the location of the variable. For example:
def_var :counter, :uint8, symbol: :_counter
The example above shows how you could access a variable from a C compiler
(which will generally be prefixed with an underscore) without having to
type the underscore in your tests. More generally, the symbol
option allows you to call a variable one thing in your firmware and call it
a different thing in your tests.
RPicSim will raise an exception if it cannot find the specified symbol in the symbol table. To troubleshoot this, you might print the list of symbols that RPicSim found:
p sim.class.program_file.var_addresses.keys
You can use the address
option to specify an arbitrary address
instead of using the symbol table. For example:
def_var :counter, :uint8, address: 0x63
Variables are assumed to be in RAM by default, but you can specify that
they are in program memory or EEPROM using the memory
option.
def_var :settings, :word, memory: :program_memory
def_var :checksum, :uint16, memory: :eeprom
Program memory on non-PIC18 devices
On non-PIC18 devices, program memory is made up of words that are 12 bits or 14 bits wide.
The type of address used for program memory of these devices is called a
word address because it specifies the number of a word instead of
the number of a byte. For example, a word address of 1
would
correspond to the second word in program memory.
To access all the bits of a particular word, you can define your variable
to be of the :word
type as shown in the example above. If you
specify any of the integer types like :uint8 or :int16, the bytes that
comprise that variable will live in the least-significant 8 bits of one or
more words in program memory. The upper bits of the words will not be
changed when writing to the variable.
This behavior is useful because if you store an integer in program memory as 1 to 4 consecutive RETLW instructions, you can read and write from it in Ruby without changing the bits that make those words be RETLW instructions.
Accessing special function registers
The Special Function Registers (SFRs) on a microcontroller enable the firmware to interact with the microcontroller's peripherals and talk to the outside world. The RPicSim::Sim#reg method can be called on your simulation object to retrieve a RPicSim::Variable object:
sim.reg(:LATA) # => returns a Variable object
If you are using RPicSim's RSpec integration, the reg
method inside an example automatically
redirects to the @sim
object:
it "works" do
reg(:LATA) # => returns a Variable object
end
The first argument of RPicSim::Sim#reg should be a symbol containing the name of the SFR. The name comes from the MPLAB X code, but we expect it to match the name given in the microcontroller's datasheet.
Note that the MPLAB X code considers “SFRs” to only be the special registers that have an address in memory. The special registers without a memory address are called Non-Memory-Mapped Registers (NMMRs). For example, on some chips, WREG and STKPTR are NMMRs. You can access NMMRs in exactly the same way as SFRs:
it "sets W to 5" do
expect(reg(:WREG).value).to eq 5
end
Using a variable
Once you have defined a variable and accessed it using one of the methods
above, you will have an instance of a subclass of RPicSim::Variable. You
can read and write the value of the variable using the value
attribute:
counter.value = 0x6A
expect(counter.value).to eq 0x6A
Protected bits
When you write to a register with RPicSim::Variable#value=, you are (according to our understanding of MPLAB X) writing to it in the same way that the simulated microcontroller would write to it. This means that some bits might not be writable or might have restrictions on what value can be written to them. For example, the TO and PD bits of the STATUS register on the PIC10F322 are not writable by the microcontroller.
To get around this, you can use RPicSim::Variable#memory_value= instead, which should allow you to write to any of the bits.
Peripheral updating
The MPLAB X code contains various objects that simulate the peripherals on
a chip, such as the ADC. We have not determined whether writing to SFRs
using the RPicSim::Variable object updates the simulation of those
peripherals in the proper way. Also, whether the peripherals get updated
might depend on whether the value
or the
memory_value
attribute is used for writing.
Addition example
This section contains a simple example showing how to apply the information above and use RPicSim::Variable objects.
Here is a minimal MPASM assembly program for the PIC10F322 that does not actually do anything but it has a 16-bit addition subroutine:
#include p10F322.inc
__config(0x3E06)
udata
x res 2
y res 2
z res 2
code 0
addition ; 16-bit addition routine: z = x + y
movf x, W
addwf y, W
movwf z
movf x + 1, W
btfsc STATUS, C
addlw 1
addwf y + 1, W
movwf z + 1
return
end
In spec/spec_helper.rb
, we make a simulation class that points
to the compiled COF file and defines the variables:
require 'rpicsim/rspec'
class Addition < RPicSim::Sim
use_device "PIC10F322"
use_file File.dirname(__FILE__) + "../firmware/dist/firmware.cof"
def_var :x, :uint16
def_var :y, :uint16
def_var :z, :uint16
end
In spec/addition_spec.rb
, we write a simple unit test that
writes to x
and y
, runs the addition
subroutine, and checks that the correct result is stored in z
:
require_relative 'spec_helper'
describe "addition routine" do
before do
start_sim Addition
end
it "can add 70 + 22" do
x.value = 70
y.value = 22
run_subroutine :addition, cycle_limit: 100
expect(z.value).to eq 92
end
end