Figure 1 : SimpleCPU_v1d1
The simpleCPU version 1d is a bit of a blank canvas, a processor that can be adapted and expanded to help illustrate different architectural features. One of these is interrupts, the ability to force the processor to stop what its doing and switch its processing time over to a different "task". To help illustrate why this is architectural feature is needed and to see it in action i decided to implement this year's must have birthday and Christmas present: the simpleCPU clock :). This project will be an incremental design. The first version will implement the basics, a hardware timer to generate the interrupts and an LCD display. Next, i will add a serial port and switches to allow the time to updated i.e. allow the user to send a serial packet specifying the time using HH:MM:SS, or by pressing different switches etc, to set the time. In addition to this the serial port can transmit time data to a terminal. Finally, to allow people to see what the time is at a distance i will add a VGA display to display the time as a digital (seven segment), or analogue display.
Version 1.0 : LCD
Hardware
Software
Version 1.1 : LCD + Serial port
Version 1.2 : LCD + Serial port + Switches
Version 1.3 : LCD + Serial port + Switches + VGA
Version 1.3 : Improving accuracy, reducing drift
The ISE project and code for this version of the simpleCPU clock can be downloaded here: (Link).
Figure 2 : version 1.0 SimpleCPU clock
To implement the first version of the clock we will need a periodic timer, a peripheral device that will generate an interrupt every second. This component is commonly referred to as a Periodic Interrupt Timer (PIT), or a Programmable Interval Timer (Link). When integrated into the clock, this component will trigger the Interrupt Service Routine (ISR) every second, allowing the processor to update the its "time" variables: seconds, minutes and hours. I could have implemented this functionality as a Real Time Clock (RTC) (Link), a peripheral device similar in functionality to the PIT, but rather than implementing the seconds, minutes, hours, days counters in software these are implemented directly in hardware within the RTC. This has the advantage that the RTC can continue to measure the passage of time when the processor is turned off i.e. typically the RTC will have a backup battery when power to the processor is turned off.
The interrupt version of the simpleCPU_v1d has a single interrupt pin, interrupting its fetch-decode-execute cycle. For more information on this processor and its interrupt handler, you can read my original mumble here: (Link). Note, there is a small bug in this design, in that you can not interrupt a jump instruction, therefore, a program that only contains a jump instruction e.g. an infinite loop at the end of a program, can not be interrupted. This is typically not an issue as we can add a dummy MOVE i.e. a no operation (NOP), but it is important to remember :). The VHDL code for the PIT is listed below:
-- =============================================================================================================
-- *
-- * File Name: timer.vhd
-- *
-- * Version: V1.0
-- *
-- * Release Date:
-- *
-- * Author(s): M.Freeman
-- *
-- * Description: Simple timer module
-- *
-- * Conditions of Use: THIS CODE IS COPYRIGHT AND IS SUPPLIED "AS IS" WITHOUT WARRANTY OF ANY KIND, INCLUDING,
-- * BUT NOT LIMITED TO, ANY IMPLIED WARRANTY OF MERCHANTABILITY AND FITNESS FOR A
-- * PARTICULAR PURPOSE.
-- *
-- * Notes:
-- *
-- =============================================================================================================
LIBRARY IEEE;
USE IEEE.STD_LOGIC_1164.ALL;
USE IEEE.STD_LOGIC_UNSIGNED.ALL;
LIBRARY UNISIM;
USE UNISIM.vcomponents.ALL;
ENTITY timer IS
PORT (
clk : IN STD_LOGIC;
clr : IN STD_LOGIC;
addr : IN STD_LOGIC_VECTOR(1 DOWNTO 0);
data_in : IN STD_LOGIC_VECTOR(15 DOWNTO 0);
data_out : OUT STD_LOGIC_VECTOR(15 DOWNTO 0);
we : IN STD_LOGIC;
ce : IN STD_LOGIC;
irq : OUT STD_LOGIC );
END timer;
ARCHITECTURE timer_arch OF timer IS
--
-- components
--
COMPONENT ld_down_cnt_tc
GENERIC (
width : INTEGER := 32 );
PORT (
clk: IN STD_LOGIC;
clr: IN STD_LOGIC;
din: IN STD_LOGIC_VECTOR(width-1 DOWNTO 0);
dout: OUT STD_LOGIC_VECTOR(width-1 DOWNTO 0);
ld: IN STD_LOGIC;
ce: IN STD_LOGIC;
en: IN STD_LOGIC;
tc: OUT STD_LOGIC;
z: OUT STD_LOGIC );
END COMPONENT;
COMPONENT reg
GENERIC (
width : INTEGER := 32 );
PORT (
clk : IN STD_LOGIC;
clr : IN STD_LOGIC;
en : IN STD_LOGIC;
rst : IN STD_LOGIC;
din : IN STD_LOGIC_VECTOR(width-1 DOWNTO 0);
dout : OUT STD_LOGIC_VECTOR(width-1 DOWNTO 0));
END COMPONENT;
COMPONENT pulse_sync
PORT (
clk: IN STD_LOGIC;
clr: IN STD_LOGIC;
pulse_i: IN STD_LOGIC;
pulse_o: OUT STD_LOGIC;
pulse_d: OUT STD_LOGIC );
END COMPONENT;
--
-- constants
--
CONSTANT max : STD_LOGIC_VECTOR(15 DOWNTO 0) := (OTHERS=>'1');
--
-- signals
--
SIGNAL GND : STD_LOGIC ;
SIGNAL VCC : STD_LOGIC ;
SIGNAL wr : STD_LOGIC ;
SIGNAL rd : STD_LOGIC ;
SIGNAL en : STD_LOGIC_VECTOR(2 DOWNTO 0);
SIGNAL cnt_i : STD_LOGIC_VECTOR(15 DOWNTO 0);
SIGNAL cnt_o : STD_LOGIC_VECTOR(15 DOWNTO 0);
SIGNAL cnt_ld : STD_LOGIC;
SIGNAL cnt_ce : STD_LOGIC;
SIGNAL cnt_en : STD_LOGIC;
SIGNAL cnt_tc : STD_LOGIC;
SIGNAL cnt_z : STD_LOGIC;
SIGNAL command : STD_LOGIC_VECTOR(7 DOWNTO 0);
SIGNAL status : STD_LOGIC_VECTOR(3 DOWNTO 0);
SIGNAL zero : STD_LOGIC;
SIGNAL enable : STD_LOGIC;
SIGNAL counter : STD_LOGIC_VECTOR(8 DOWNTO 0);
SIGNAL clock_div_2 : STD_LOGIC;
SIGNAL clock_div_4 : STD_LOGIC;
SIGNAL clock_div_8 : STD_LOGIC;
SIGNAL clock_div_16 : STD_LOGIC;
SIGNAL clock_div_32 : STD_LOGIC;
SIGNAL clock_div_64 : STD_LOGIC;
SIGNAL clock_div_128 : STD_LOGIC;
SIGNAL clock_div_256 : STD_LOGIC;
SIGNAL clock_div_512 : STD_LOGIC;
SIGNAL clock_en : STD_LOGIC;
SIGNAL clock_pulse : STD_LOGIC;
TYPE state_type IS (S0, S1, S2, S3);
SIGNAL present_state, next_state: state_type;
BEGIN
--
-- signal buffers
--
GND <= '0';
VCC <= '1';
wr <= we and ce;
rd <= (not we) and ce;
status <= cnt_en & cnt_tc & cnt_z & zero;
zero <= cnt_z and command(0);
--
-- processes
--
-- 0xFF3
-- 0xFF2 status register
-- 0xFF1 command register
-- 0xFF0 count register
--
-- write address decoder
--
input_data_decoder : PROCESS( addr, wr )
BEGIN
IF wr='1'
THEN
CASE addr IS
WHEN "00" => en <= "001"; -- count reg
WHEN "01" => en <= "010"; -- command reg
WHEN "10" => en <= "100"; -- status
WHEN OTHERS => en <= "000";
END CASE;
ELSE
en <= "000";
END IF;
END PROCESS;
--
-- read output mux
--
output_data_decoder : PROCESS( addr,
cnt_o,
command, status )
BEGIN
CASE addr IS
WHEN "00" => data_out <= cnt_o;
WHEN "01" => data_out <= "00000000" & command;
WHEN "10" => data_out <= "00000000" & "0000" & status;
WHEN OTHERS => data_out <= (OTHERS=>'0');
END CASE;
END PROCESS;
--
-- clock divider network
--
clock_divider : PROCESS( clk, clr )
BEGIN
IF clr='1'
THEN
counter <= (OTHERS=>'0');
ELSIF clk='1' and clk'event
THEN
counter <= counter + 1;
END IF;
END PROCESS;
clock_div_2 <= counter(0);
clock_div_4 <= counter(1);
clock_div_8 <= counter(2);
clock_div_16 <= counter(3);
clock_div_32 <= counter(4);
clock_div_64 <= counter(5);
clock_div_128 <= counter(6);
clock_div_256 <= counter(7);
clock_div_512 <= counter(8);
clock_pulse_mux : PROCESS( command, clock_div_2,
clock_div_4, clock_div_8, clock_div_16,
clock_div_32, clock_div_64, clock_div_128,
clock_div_256, clock_div_512 )
BEGIN
CASE command(7 DOWNTO 4) IS
WHEN "0000" => clock_pulse <= clock_div_2;
WHEN "0001" => clock_pulse <= clock_div_4;
WHEN "0010" => clock_pulse <= clock_div_8;
WHEN "0011" => clock_pulse <= clock_div_16;
WHEN "0100" => clock_pulse <= clock_div_32;
WHEN "0101" => clock_pulse <= clock_div_64;
WHEN "0110" => clock_pulse <= clock_div_128;
WHEN "0111" => clock_pulse <= clock_div_256;
WHEN "1000" => clock_pulse <= clock_div_512;
WHEN OTHERS => clock_pulse <= '0';
END CASE;
END PROCESS;
--
-- counter enable
--
clock_en_pulse : pulse_sync PORT MAP(
clk => clk,
clr => clr,
pulse_i => clock_pulse,
pulse_o => clock_en,
pulse_d => OPEN );
--
-- command register
--
-- Bit 7 : clock select msb
-- Bit 6 : clock select
-- Bit 5 : clock select
-- Bit 4 : clock select lsb
-- Bit 3 : nu
-- Bit 2 : nu
-- Bit 1 : single shot (0) or auto reload (1)
-- Bit 0 : enable count (1=true, 0=false)
--
command_register : PROCESS(clk, clr)
BEGIN
IF clr='1'
THEN
command <= "00000000";
ELSIF clk='1' and clk'event
THEN
IF en(1)='1'
THEN
command <= data_in(7 downto 0);
END IF;
END IF;
END PROCESS;
--
-- count data register
--
data_reg : reg
GENERIC MAP(
width => 16 )
PORT MAP(
clk => clk,
clr => clr,
en => en(0),
rst => GND,
din => data_in,
dout => cnt_i);
--
-- counter
--
cnt : ld_down_cnt_tc
GENERIC MAP(
width => 16 )
PORT MAP(
clk => clk,
clr => clr,
din => cnt_i,
dout => cnt_o,
ld => cnt_ld,
ce => cnt_ce,
en => clock_en,
tc => cnt_tc,
z => cnt_z );
--
-- state machine
--
sync: PROCESS(clk, clr)
BEGIN
IF clr='1'
THEN
present_state <= S0;
ELSIF clk'event and clk='1'
THEN
present_state <= next_state;
END IF;
END PROCESS;
comb: PROCESS(present_state, command, en, cnt_z, cnt_tc)
BEGIN
cnt_ld <= '0';
cnt_ce <= '0';
cnt_en <= '0';
enable <= '0';
CASE present_state IS
-- wait for enable update
--
WHEN S0 =>
cnt_ld <= '0';
cnt_ce <= '0';
cnt_en <= '0';
IF en(1)='1'
THEN
next_state <= S1;
ELSE
next_state <= S0;
END IF;
-- test enable
--
WHEN S1 =>
cnt_ld <= command(0);
cnt_ce <= command(0);
cnt_en <= '0';
IF command(0)='1'
THEN
next_state <= S2;
ELSE
next_state <= S0;
END IF;
-- count down
--
WHEN S2 =>
cnt_ld <= '0';
cnt_ce <= '1';
cnt_en <= '1';
enable <= cnt_z and command(0);
IF en(1)='1'
THEN
next_state <= S1;
ELSIF cnt_z='1' and command(1)='0'
THEN
next_state <= S0;
ELSIF cnt_z='1' and command(1)='1'
THEN
next_state <= S3;
ELSE
next_state <= S2;
END IF;
-- reload
--
WHEN S3 =>
cnt_ld <= '1';
cnt_ce <= '1';
cnt_en <= '0';
next_state <= S2;
--default condition
--
WHEN OTHERS =>
cnt_ld <= '0';
cnt_ce <= '0';
cnt_en <= '0';
next_state <= S0;
END CASE;
END PROCESS;
--
-- interrupt pulse
--
irq_en_pulse : pulse_sync PORT MAP(
clk => clk,
clr => clr,
pulse_i => enable,
pulse_o => irq,
pulse_d => OPEN );
END timer_arch;
This VHDL code and its sub-components can be downloaded here: (Link). The timer component contains three memory mapped registers shown below: command, status and count. The command register allows the user to select a pre-scalar from 5MHz to 19KHz. In addition to this the programmer can select between one-shot, or automatic reload count modes, and to enable / disable the timer. The pre-scalar clock is used by the timer's 16 bit counter. The desired delay counter is loaded into the counter, this is then decremented at the pre-scalar clock speed, an interrupt pulse being generated when the count reaches zero.
################### # TIMER REGISTERS # ################### # COMMAND REGISTER # --------------- # B7 : clock select msb # B6 : clock select # B5 : clock select # B4 : clock select lsb # B3 : nu # B2 : nu # B1 : single shot (0) or auto reload (1) # B0 : enable count (1=true, 0=false) # CLOCK SELECT BITS # ----------------- # 0000 : clock_div_2 5MHz 0.0000002 200 ns # 0001 : clock_div_4 2.5MHz 0.0000004 400 ns # 0010 : clock_div_8 1.25MHz 0.0000008 800 ns # 0011 : clock_div_16 625KHz 0.0000016 1.6 us # 0100 : clock_div_32 312.5KHz 0.0000032 3.2 us # 0101 : clock_div_64 156.25KHz 0.0000064 6.4 us # 0110 : clock_div_128 78.125KHz 0.0000128 12.8 us # 0111 : clock_div_256 39.0625KHz 0.0000256 25.6 us # 1000 : clock_div_512 19.53125KHz 0.0000512 51.2 us # STATUS REGISTER # --------------- # B7 : nu # B6 : nu # B5 : nu # B4 : nu # B3 : counter enabled # B2 : counter terminal count # B1 : counter zero # B0 : zero (counter zero & count enabled) # COUNT REGISTER # -------------- # 16bit loadable count down counter # Note, clock speed is 10MHz # 1 / 0.0000512 = 19531 error = 0.25 * 51.2 us = 12.8 us per second # error in 24 hours = 24 * 60 * 60 * 12.8us = 1.10592 seconds per day
For a one second delay a pre-scalar of 512 is used, producing a clock of 19KHz i.e. a period of 51.2us. The counter is loaded with the value of 19531, producing a delay of approximately 1 second. However, as the combination of pre-scalar clock and counter does not produce an exact 1 second delay, each "1 second" delay will have an error of +12.8us. Resulting in a 1.10592 second error each day. A simple solution to this would be to subtract 1 second from the seconds variable used to store the current time each day. However, these calculations do not include the code needed to implement the interrupt service routine (ISR) e.g. increment the seconds, minutes and hours variables. This delay is tricky to calculate as the path through the ISR will vary depending on whether the minutes and hours variables are updated i.e. each machine level instruction taken 0.3us to execute on the simpleCPU, therefore, when the timing errors is measured in micro-seconds adjustments are more of an art than a science :). The hardware used to construct version 1 of the clock is shown in figure 3:
Figure 3 : version 1.0 SimpleCPU clock schematic
The control registers used by the interrupt handler, GPIO and UART are listed below. For more information on this processor and its interrupt handler: (Link). IRQ 0 has the highest priority, IRQ7 the lowest.
################# # IRQ REGISTERS # ################# # COMMAND REGISTER # ---------------- # B7 : irq_7 enable (1=enable, 0=disable) # B6 : irq_6 enable (1=enable, 0=disable) # B5 : irq_5 enable (1=enable, 0=disable) # B4 : irq_4 enable (1=enable, 0=disable) # B3 : irq_3 enable (1=enable, 0=disable) # B2 : irq_2 enable (1=enable, 0=disable) # B1 : irq_1 enable (1=enable, 0=disable) # B0 : irq_0 enable (1=enable, 0=disable) # TRIGGER REGISTER # ---------------- # when read returns one-hot representation of highest priority IRQ (0 high, 7 low) # with read complete IRQ flag reset. In the case of multiple IRQ set the next highest # IRQ is selected as the new trigger source. # STATUS REGISTER # --------------- # when read returns a binary value of all IRQ states, this is cleared when written to. # IRQ # --- # B7 : NU # B6 : NU # B5 : NU # B4 : NU # B3 : NU # B2 : TIMER # B1 : UART # B0 : GPIO
The output port of the GPIO component is used to control the LCD 16 X 2 display, a 4bit bus and 3 control lines. The input port is connected to four switches and four push buttons, each with internal pull-ups i.e. when no switch/button is pressed the input value is 0xFF. Note, input port pins are inverted and ORed together to drive IRQ out.
################## # GPIO REGISTERS # ################## # OUTPUT PINS # ----------- # 128 B7 : NU # 64 B6 : RW (0) # 32 B5 : E # 16 B4 : RS # 8 B3 : D7 # 4 B2 : D6 # 2 B1 : D5 # 1 B0 : D4 # INPUT PINS # ---------- # 128 B7 : BUT3 # 64 B6 : BUT2 # 32 B5 : BUT1 # 16 B4 : BUT0 # 8 B3 : SW3 # 4 B2 : SW2 # 2 B1 : SW1 # 1 B0 : SW0
The UART used is a cut down version of that used in the Pong game (Link). Functionally, the same, but limited to 9600 bits/second so that it can use the 10MHz clock used by the processor. Note, IRQ out set by RX_VALID, did think of also adding hardware to generate an interrupt when TX is idle, however, i decided this functionality wasn't needed for this application.
################## # UART REGISTERS # ################## # TX : B7 - B0 data # RX : B7 - B0 data # STATUS REGISTER # --------------- # B7 : NU # B6 : NU # B5 : NU # B4 : NU # B3 : NU # B2 : TX Idle # B1 : RX Idle # B0 : RX Valid
The memory map of the system shown in figure 3 is listed below i.e. the system contains four peripheral devices, mapped into the top of memory.
############## # MEMORY MAP # ############## # ADDR CS WR RD # 0xFFF 3 IRQ COMMAND IRQ COMMAND # 0xFFE 3 NU IRQ TRIGGER # 0xFFD 3 IRQ STATUS IRQ STATUS # 0xFFC 3 NU NU # 0xFFB 2 GPIO A out GPIO A out # 0xFFA 2 GPIO A out GPIO A in # 0xFF9 2 GPIO A out GPIO A out # 0xFF8 2 GPIO A out GPIO A in # 0xFF7 1 UART TX UART RX # 0xFF6 1 UART TX UART STATUS # 0xFF5 1 UART TX UART RX # 0xFF4 1 UART TX UART STATUS # 0xFF3 0 NU NU # 0xFF2 0 NU TIMER STATUS # 0xFF1 0 TIMER COMMAND TIMER COMMAND # 0xFF0 0 TIMER COUNT TIME COUNT # 0xFEF MEM RAM RAM # ... ... ... # 0x000 MEM RAM RAM
The main program configures the LCD display, then enables a single IRQ source i.e. timer, with a 1 second delay. The program enters a loop, comparing the current seconds count with the previous second count. This loop is exited when the ISR increments the current seconds counter. The main program then updates the previous seconds count variable with the new value and displays the time on the LCD.
################
# MAIN PROGRAM #
################
start:
call lcd_init # configure LCD and display text string
call lcd_line_2
move ra 0x04
store ra IRQ_COMMAND # enable TIMER irq, disable GPIO and UART irq
load ra count
store ra TIMER_COUNT # init counter with 1 second delay count
move ra 0x83
store ra TIMER_COMMAND # set clock/512, auto reload, enable timer.
wait:
load ra previous # is current seconds value = previous seconds value
subm ra seconds # if yes, wait
jumpz wait
load ra seconds # update previous seconds value
store ra previous
call lcd_line_2 # set LCD cursor back to the start of line 2
call displayTime # display time i.e. HH:MM:SS
jump wait # repeat
trap:
move RA 0 # dummy trap
jump trap
The interrupt service routine (ISR) starts at address 0x064. Note, the ISR address can be selected by the programmer. The chosen address is used to set a constant component within the PC i.e. the programmer can update the interrupt vector in hardware, such that when an interrupt occurs the processor will jump to this address. Inside the ISR the value of RA is first saved to memory i.e. checkpointed, such that the main program's state is not corrupted. Then the timer's interrupt flag is reset by reading IRQ_TRIGGER, as there is only one interrupt source this does not need to be decoded. Next the seconds, minutes and hours variables are updated before restoring the value of RA, before control being returned back to the main program.
#######
# ISR #
#######
.addr 100
isr:
store RA checkpoint # checkpoint RA
load RA IRQ_TRIGGER # reset IRQ
load RA seconds # inc seconds
add RA 1
store RA seconds
sub RA 60
jumpnz isr_exit
move RA 0 # inc minutes
store RA seconds
load RA minutes
add RA 1
store RA minutes
sub RA 60
jumpnz isr_exit
move RA 0 # inc hours
store RA minutes
load RA hours
add RA 1
store RA hours
sub RA 24
jumpnz isr_exit
move RA 0
store RA hours
isr_exit:
load RA seconds # load HH:MM:SS into register RD so that it can be seen
move RD RA # in the simulation when testing.
load RA minutes
move RD RA
load RA hours
move RD RA
load RA checkpoint # restore the value of RA
reti
To display the time on the LCD the displayTime subroutine is used. This generates an ASCII text string lcd_time that will be displayed on the LCD. The seconds, minutes and hours variables are converted into two decimal digits using the byte2Dec_digit sbroutine. It takes approximately 70ms for the simpleCPU to update the LCD, this is mainly due to the LCD clock speed. I could increase this speed by adjusting the software time delay used in the LCD subroutines, but as this is significantly faster than 1 second interrupt rate, and the processor doesn't have anything else to do i decided not play around with something that worked i.e. KISS :)
#####################
# CLOCK SUBROUTINES #
#####################
# ----------------
# - Display time -
# ----------------
displayTime:
movea( RC, lcd_time ) # set RC to string address
load RA hours # convert hour digits
call byte2Dec_digit
add RC 1 # inc pointer
move RA 0x3A # write ":" to string
store RA (RC)
add RC 1 # inc pointer
load RA minutes # convert minute digits
call byte2Dec_digit
add RC 1 # inc pointer
move RA 0x3A # write ":" to string
store RA (RC)
add RC 1 # inc pointer
load RA seconds # convert second digits
call byte2Dec_digit
add RC 1 # inc pointer
move RA 0 # write NULL char to string to terminate
store RA (RC)
move ra 0 # set lcd and config variables used by lcdTxString
store RA lcd_config # subroutine
movea( RA, lcd_time )
store RA lcd_pntr
call lcdTxString # display string on LCD
ret
byte2Dec_digit:
move RB 0
byte2Dec_digit_loop:
sub RA 10 # sub 10
jumpc byte2Dec_digit_exit # yes, exit
add RB 1 # inc count
jump byte2Dec_digit_loop # repeat
byte2Dec_digit_exit:
add RB 0x30 # add 0x30 to convert value to ASCII
store RB (RC)
move RD RB
add RC 1
add RA 10 # undo last sub
add RA 0x30 # add 0x30 to convert value to ASCII
move RD RA
store RA (RC)
ret
The code for version 1.0 of the simpleCPU clock can be downloaded here: clock_v1_0.asm. A short video of the clock in action is available here: Link, which can only be described as riveting, a lot better than watching paint dry :).
A clock that you can set the time isn't that useful, i guess its more of a timer than a clock. Therefore, as the original simpleCPU_v1.1 base system comes with a serial port, i decided to use this peripheral to set the time. The serial port connector (DCE) is shown in figure 4, running at 9600 bits/s.
Figure 4 : serial port
The user connects to the FGPA board using its serial port connector, sending a text string using the following format: HHMMSS<CR>, some examples are shown below:
The clock uses a 24 hour format. The time variables i.e. H, M and S are updated within the clock when the user presses the Enter key i.e. sends the <CR> character, the value 0x0D. There will obviously be some delay between the user pressing the Enter key and the processor detecting this character, however, i did not see this nonrecurring delay as being significant. The serial port code can be implemented using interrupts i.e. the UART will generate an interrupt when a character is received, but for this version i decided to keep things simple and went for a polled implementation. The main subroutines used are readSerialPort and updateTime, these are non-blocking subroutines and are listed below:
# --------------------
# - Read serial port -
# --------------------
# serial packet format: HHMMSS<CR>
readSerialPort:
load RA UART_STATUS # test status bit
and RA 1
jumpz readSerialPort_exit
load RA UART_RX # read char, buffer in RB
move RB RA
#store RA UART_TX
movea( RC, rxBuffer ) # set pointer to rxBuf
load RA rxBufferIndex # calc offset
add RC RA
store RB (RC) # store data
add RA 1
store RA rxBufferIndex # inc pntr
sub RA 10
jumpz readSerialPort_update # have 10 chars been entered?
sub RB 0x0D
jumpnz readSerialPort_exit # has cr been RX?
readSerialPort_update:
movea( RB, rxBuffer ) # debug, dump string back to terminal
load RA rxBufferIndex # comment out in final design
add RA RB
move RC 0
store RC (RA)
call tx_loop
call updateTime # update hours, minutes and seconds variables
move RA 0 # reset pntr
store RA rxBufferIndex
readSerialPort_exit:
ret
# --------------------------
# - Update time (HH:MM:SS) -
# --------------------------
updateTime:
move RA 0
store RA rxBufferIndex
movea( RB, rxBuffer ) # string address, first char
call updateDigits # convert char to int
load RA digit1 # add digits
addm RA digit0
store RA hours # update hours
movea( RB, rxBuffer ) # string address, third char
load RA rxBufferIndex
add RB RA
call updateDigits # convert char to int
load RA digit1 # add digits
addm RA digit0
store RA minutes # update minutes
movea( RB, rxBuffer ) # string address, fifth char
load RA rxBufferIndex
add RB RA
call updateDigits # convert char to int
load RA digit1 # add digits
addm RA digit0
store RA seconds # update seconds
ret
# Convert two CHARs into digits
# -----------------------------
updateDigits:
load RA (RB) # addr of char passed in RB i.e. 10s digit
sub RA 0x30 # sub 0x30 to convert to int
store RA digit1
rol RA # x2
rol RA # x4
rol RA # x8
addm RA digit1 # x9
addm RA digit1 # x10
store RA digit1
add RB 1 # addr of second char
load RA (RB) # addr of second char in RB i.e. 1s digit
sub RA 0x30 # sub 0x30 to convert to int
store RA digit0
load RA rxBufferIndex # inc pntr
add RA 2
store RA rxBufferIndex
ret
Decided not to use the serial port to send the time to the user, as i thought this would be make the process of sending the time tricky i.e. difficult to type out digits if you are receiving a text string every second. The code for version 1.1 of the simpleCPU clock can be downloaded here: clock_v1_1.asm.
The FPGA board does have a rotary encoder which would have been a nice way to adjust the time, however, the initial system only has GPIO, so for this version is going to use push buttons shown in figure 5. The EAST button will increment the minutes value, the WEST button will decrement the minutes value, overflows and underflows incrementing / decrementing the hours value. The seconds value is simply zeroed when a button is pressed.
Figure 5 : push buttons
One complexity that caused quite a bit of head scratching was the CALL/RET stack :). The simpleCPUv1d's CALL/RET stack is limited to four values i.e. normally this means four nested subroutine calls. However, i forgot that one of these slots has to be reserved for the interrupt. Therefore, when i tried to use previously tested code very strange things started to happen, as this code was developed on a version of the simpleCPUv1d that did not use interrupts i.e. it was using all four levels of nesting for subroutines, leaving no space for the interrupt. This resulted in a stack overflow, which is hard to detect when you are not looking for it :). To get round this issue i converted one of the LCD display subroutines to a macro, freeing space on the stack. Disadvantage of this is that it does increase code size a little. The main program loop has now been updated with a call to readSwitch, as shown below:
wait:
call readSerialPort # test serial
call readSwitch # test switches
load ra currentSeconds # has seconds been updated
subm ra seconds
jumpz wait
load ra seconds # yes, update seconds
store ra currentSeconds
update:
call lcd_line_2 # update display
call displayTime
jump wait
The readSwitch subroutine is shown below. To free space on the the stack for subroutines i.e. to stop an interrupt causing a CALL/RET stack overflow, interrupts are disabled. Contact bounce issues are handled by an initial delay loop, ensuring that any key press has been stable for 3ms. This delay could be extended if needed, however, when tested no switch press "glitches" were detected. The code then determines which switch has been pressed, increments or decrements the minutes and hours variables, seconds are zeroed. Then a 150ms software delay is performed to slow down the update rate, so that the user can select / see a specific time on the LCD display. Control then returns back to the start of this subroutine where the switch state is retested and the inc / dec funtion repeated i.e. the user can single step or hold down the button to repeatedly update the time value.
# -----------------
# - Read switches -
# -----------------
# 64 UP
# 32 DOWN
readSwitch:
move ra 0x00 # disable irq
store ra IRQ_COMMAND
movea( RB, 0xFFF ) # debounce delay
readSwitch_loop:
load RA GPIO_A_IN # test if input is high
and RA 0x60
jumpz readSwitch_exit
sub RB 1
jumpnz readSwitch_loop
load RA GPIO_A_IN # up or down button pressed
and RA 0x60
sub RA 0x40
jumpz readSwitch_inc
readSwitch_dec:
move RA 0 # set second to 0
store RA seconds
load RA minutes # is minutes 0?
and RA 0xFF
jumpz readSwitch_dec_min
sub RA 1 # no dec
store RA minutes
jump readSwitch_update
readSwitch_dec_min:
move RA 59 # yes set to 59
store RA minutes
load RA hours # is hours zero?
and RA 0xFF
jumpz readSwitch_dec_hour
sub RA 1 # no dec
store RA hours
jump readSwitch_update
readSwitch_dec_hour:
move RA 23 # yes set to 23
store RA hours
jump readSwitch_update
readSwitch_inc:
move RA 0 # set second to 0
store RA seconds
load RA minutes # inc minutes
add RA 1
store RA minutes
sub RA 60
jumpnz readSwitch_update
move RA 0 # set minutes to 0
store RA minutes
load RA hours # inc hours
add RA 1
store RA hours
sub RA 24
jumpnz readSwitch_update
move RA 0 # set hours to 0
store RA hours
readSwitch_update:
call lcd_line_2 # update display
call displayTime
move RA 40 # set update rate
store RA delay_count
movea( RB, 0xFFF )
readSwitch_update_loop:
sub RB 1
jumpnz readSwitch_update_loop
movea( RB, 0xFFF )
load RA delay_count
sub RA 1
store RA delay_count
jumpnz readSwitch_update_loop
jump readSwitch
readSwitch_exit:
move ra 0x04
store ra IRQ_COMMAND # enable TIMER IRQ
ret
The code for version 1.2 of the simpleCPU clock can be downloaded here: clock_v1_2.asm.
WORK IN PROGRESS
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
Contact email: mike@simplecpudesign.com