A Scheme Interpreter for ARM Microcontrollers:
Stellaris LM3Sxxxx-EVB Program Examples, Version 00.0160

SourceForge.net Logo


The code on this page presents a multi-part example aimed at Stellaris LM3Sxxxx MCUs, more specifically the LM3S1968-EVB and LM3S6965-EVB boards (those with OLED displays). It is, example-wise, an addition to code presented on the Common Examples page. Here, some system parameters, OLED functions, ADC functions and PWM functions are specified and written to on-chip MCU files. In the end, a boot file is built that loads the other files and thus initializes the system accordingly when it is reset. The code can be used as a slow oscilloscope, with maximum sampling frequency of 250 Hz, or for the digital implementation of a PID-type controller. It may be useful to remember that the UP button is used to override the boot during reset on the LM3S1968-EVB while it is the Select button that is used for this purpose on the LM3S6965-EVB (hold that button while pressing reset to prevent execution of the boot file, if needed).


System Constants:

To start, useful system constants (peripheral base addresses and offsets) and basic functions are written to a file named "init-sys". The functions include LOS that loads data from a register, performs a logical or between the data and an input value, and stores the result back in the same register. Also included is bset that sets just one bit into a given register, with the bit specified by its position (eg. bit 0, bit 3, ...).

; open a file for system constants and functions
(define p (open-output-file "init-sys"))

; write system constants to file
  (define sysc   #x0400fe00)
  (define gioa   #x04000400)
  (define gioc   #x04000600)
  (define giod   #x04000700)
  (define giog   #x04002600)
  (define gioh   #x04002700)
  (define adc    #x04003800)
  (define pwm    #x04002800)
  (define ssi0   #x04000800)
  (define ssi-data   #x08)
  (define ssi-status #x0C)
  (define timer0 #x4003000)
  (define timer-control #x0C)
  (define timer-config  #x00)
  (define timer-mode    #x04)
  (define timer-period  #x28)
  (define timer-imask   #x18)
  (define timer-count   #x48))

; write utility functions to file
    (define (LOS val reg ofst)
      (write (logior val (read reg ofst)) reg ofst))
    (define (bset bit reg ofst) (LOS (ash 1 bit) reg ofst))
    (define (set-pin port pin)
      (write #xff port (ash 1 (+ pin 2))))
    (define (clear-pin port pin)
      (write #x00 port (ash 1 (+ pin 2))))
    (define (stop timer)
      (write #x00 timer timer-control))
    (define (restart timer)
      (write #x01 timer timer-control)))

; close the file
(close-output-port p)



The configuration of Analog to Digital Converters (ADCs) is stored in a file named "init-adc". The initialization enables ADC channels 0 to 3 with 8-times oversampling. The file also defines the function read-adc used to read the values obtained by individual ADCs (eg. (read-adc 1)).

; open the ADC initialization file
(define p (open-output-file "init-adc"))

; configure the ADCs and sequencer
  (bset 16 sysc #x0100)  ; RCGC0 <- bit 16, enable ADC clock (125k sample/sec)
  (write #x00 adc #x00)  ; ADCACTSS  <- 0, disable sample sequencers
  (write #x00 adc #x14)  ; ADCEMUX   <- 0, all adcs triggered by mcu
  (write #x03 adc #x30)  ; ADCSAC    <- 3, over-sample 8 times
  (write #x00 adc #x40)  ; ADCSSMUX0 <- 0, sequence 0 samples ADC0 only
  (write #x02 adc #x44)  ; ADCSSCTL0 <- 2, sequence 0 ends after 1 sample
  (write #x01 adc #x60)  ; ADCSSMUX1 <- 1, sequence 1 samples ADC1 only
  (write #x02 adc #x64)  ; ADCSSCTL1 <- 2, sequence 1 ends after 1 sample
  (write #x02 adc #x80)  ; ADCSSMUX2 <- 2, sequence 2 samples ADC2 only
  (write #x02 adc #x84)  ; ADCSSCTL2 <- 2, sequence 2 ends after 1 sample
  (write #x03 adc #xa0)  ; ADCSSMUX3 <- 3, sequence 3 samples ADC3 only
  (write #x02 adc #xa4)  ; ADCSSCTL3 <- 2, sequence 3 ends after 1 sample
  (write #x0f adc #x00)) ; ADCACTSS  <- f, enable all sample sequencers

; define the read-adc function
'(define (read-adc chan)
  (write (ash 1 chan) adc #x028)
  (let loop ((stat (read adc (+ #x4c (ash chan 5)))))
     (if (zero? (logand stat #x00100))
       (read adc (+ #x48 (ash chan 5)))
       (loop (read adc (+ #x4c (ash chan 5)))))))

; close the file
(close-output-port p)



Initialization of the OLED display is stored in a file named "init-oled". The file configures system pins, turns the display on and defines two functions for the user: pixel and fill. Pixel is used to write a pixel to the display and fill is used to fill a rectangle on the display (including clearing the display). The MCU pins used to connect to the OLED are different on the LM3S1968 EVB and LM3S6965 EVB. Be sure to write to file only the definition of the pins used on your board. These pin definitions are found right after the statement used to open the initialization file (choose just one of the blocks of 6 lines of code following the "define which pins are used to talk to the OLED" comments).

; open the OLED initialization file
(define p (open-output-file "init-oled"))

; define which pins are used to talk to the OLED
; LM3S1968 EVB oled: data/cmd control = port H, pin 2
;                    15V PWR enable   = port H, pin 3
    (define odc-port gioh)
    (define odc-pin  2)
    (define opw-pin  3))

; define which pins are used to talk to the OLED
; LM3S6965 EVB oled: data/cmd control = port C, pin 7
;                    15V PWR enable   = port C, pin 6
    (define odc-port gioc)
    (define odc-pin  7)
    (define opw-pin  6))

; configure system pins for OLED operation
; SysCtl, RCGC1 <- enable SSI0
; SysCtl, RCGC2 <- enable GPIO A (PA2 CLK, PA3 CS, PA5 Tx)
; SysCtl, RCGC2 <- enable GPIO C or H (PC7/PH2 dat/cmd, PC6/PH3 PWR)
; Configure SSI0 function:
; Port A, Pins 2 CLK, 3 CS, 5 Tx, AFSEL, DEN, PUR, 8mA
; Configure GPIO function:
; Port H/C, Pins 2/7 Dat/Cmd, 3/6 PWR, DIR, DEN, 8mA
; Enable OLED power
; SSICR1  <- disable SSI0, set master mode
; SSICPSR <- 10 (50MHz/10 base)
; SSICR0  <- 1Mbit (5MHz/5), PHA=POL=1, 8bit
; SSICR1  <- enable SSI0, set master mode
    (LOS #x10 sysc #x104)
    (LOS #x01 sysc #x108)
    (LOS (if (= odc-port gioh) #x80 #x04) sysc #x108)
    (let ((pssi #x2c))
      (LOS pssi gioa #x420)
      (LOS pssi gioa #x51C)
      (LOS pssi gioa #x510)
      (LOS pssi gioa #x508))
    (let ((pins (logior (ash 1 odc-pin) (ash 1 opw-pin))))
      (LOS pins odc-port #x400)
      (LOS pins odc-port #x51C)
      (LOS pins odc-port #x508)
      (write #xff odc-port (ash pins 2)))
    (write   #x00 ssi0 #x04)
    (write   #x0A ssi0 #x10)
    (write #x04C7 ssi0 #x00)
    (write   #x02 ssi0 #x04))

; initialize the display
  (define (wcmd . cmds)
    (clear-pin odc-port odc-pin)
    (for-each wssi cmds))
  (define (wssi val . n)
    (if (not (zero? (logand #x02 (read ssi0 ssi-status))))
      (write val ssi0 ssi-data)
        (write (if (null? n) 0 (car n)))
        (wssi val (if (null? n) 1 (+ (car n) 1))))))
  (wcmd #xFD #x12 #xAE #xA8 #x5f #x81 #xb7 #x82 #x3f #xA0 #x52 #xA1 #x00 #xA2)
  (wcmd #x00 #xA4 #xB1 #x11 #xB2 #x23 #xB3 #xe2 #xB7 #xBB #x01 #xBC #x20 #xAF))

; function to set a pixel on the screen
  '(define (pixel x y col)
    (clear-pin odc-port odc-pin)
    (write #x15 ssi0 ssi-data)
    (write (ash x -1) ssi0 ssi-data)
    (write (ash x -1) ssi0 ssi-data)
    (write #x75 ssi0 ssi-data)
    (write y ssi0 ssi-data)
    (write y ssi0 ssi-data)
    (set-pin odc-port odc-pin)
    (write (if (even? x 1) (ash col 4) col) ssi0 ssi-data))

; function to fill a rectangle on screen
  '(define (fill x1 y1 x2 y2 col)
    (wcmd #x15 (ash x1 -1) (ash x2 -1) #x75 y1 y2)
    (set-pin odc-port odc-pin)
    (do ((n (* (- (ash x2 -1) (ash x1 -1) -1) (- y2 y1 -1)) (- n 1))
         (clr (logior col (ash col 4))))
        ((zero? n) #t)
         (write clr ssi0 ssi-data)))

; clear the display
  '(fill 0 0 127 127 0)

; close the file
(close-output-port p)



The system being implemented is meant to be able to perform like an oscilloscope and also to control some output lines. The file "init-control" is used to define data vectors useful in these operations (eg. to erase prior on-screen traces and keep track of sampling times). It also defines the convolve function which is useful for implementing digital control systems as linear filters. The function scan is used to start scanning ADC 1 and 2 and produce traces on-screen. It takes as input argument the number of clock ticks to wait between samples. A timer is then initialized that generates one tick every 20 nano second (i.e. 50 MHz) such that specifying 500000 as input value for scan produces a 100 Hz sampling and display frequency for the ADCs.

; open the control initialization output file
(define p (open-output-file "init-control"))

; define data vectors used for data acquisition and control
  (define R_in (make-vector 128))
  (define F_in (make-vector 128))
  (define E_in (make-vector 128))
  (define Cout (make-vector 128))
  (define tdat (make-vector 128)))

; define the convolve function used (much later) for control
    (define (convolve v1 v2 i2)
      (do ((n1  1 (+ n1 1))
           (n2 (- (if (zero? i2) (vector-length v2) i2) 1)
               (- (if (zero? n2) (vector-length v2) n2) 1))
           (r (* (vector-ref v1 0) (vector-ref v2 i2))
              (+ r (* (vector-ref v1 n1) (vector-ref v2 n2)))))
          ((= n1 (vector-length v1)) r)))
    (define (wait strt dt)
      (let loop ((tm (- strt (read timer0 timer-count))))
        (if (or (>= tm dt) (and (negative? tm) (> (+ 50000000 tm) dt)))
           (loop (- strt (read timer0 timer-count)))))))

; define the scan function to get and display adc 1 and 2 data
'(define (scan T)
    (do ((n 0 (+ n 1))
         (tprv 0 (vector-ref tdat n)))
        ((= n 128) #t)
      (pixel n (vector-ref R_in n) 0)
      (pixel n (vector-ref F_in n) 0)
      (wait tprv T)
      (vector-set! tdat n (read timer0 timer-count))
      (vector-set! R_in n (- 80 (ash (read-adc 1) -4)))
      (vector-set! F_in n (- 80 (ash (read-adc 2) -4)))
      (pixel n (vector-ref R_in n) #x03)
      (pixel n (vector-ref F_in n) #x0f))
  (scan T))

; configure a 20 nano-second timer to pace the scans
    (stop timer0)
    (write #x00 timer0 timer-config)
    (write #x02 timer0 timer-mode)
    (write 50000000 timer0 timer-period)
    (restart timer0))

; close the file
(close-output-port p)


Control File:

For control purposes, we may want to actuate PWM output lines at various duty cycles. The "control" file configures PWM 0 and 1 for this purpose (first the pins, then the peripherals). It then defines a function named set-duty to easily change the duty cycle of both PWM 0 and 1 (simultaneously) to values between 0 and 1023 that represent duty cycles of 0 to 100%. Note that on the LM6965-EVB, PWM0 is connected to the onboard speaker and thus, modifying the PWM duty cycle audibly alters the sound level produced. The code in "control" proceeds to define and clear data vectors used for control operations and then defines the control function that convolves coefficient vectors over an error signal (difference between ADC 1 and 2 inputs) and over a past control signals to obtain an updated control signal which it then uses to set the PWM duty ratio (standard approach to digital control).

; open the control output file
(define p (open-output-file "control"))

; configure PWM pin function
    (bset 20 sysc #x0100) ; RCGC0 <- bit 20, enable PWM block clock
    (bset  3 sysc #x0108) ; RCGC2 <- bit  3, enable GPIOD pad clock (PWM1 pin)
    (bset  6 sysc #x0108) ; RCGC2 <- bit  6, enable GPIOG pad clock (PWM0 pin)
    (bset  1 giod #x420)  ; GPIOD AFSEL <- bit 1, PWM1 function for pin PD1
    (bset  2 giog #x420)  ; GPIOG AFSEL <- bit 2, PWM0 function for pin PG2
    (bset  1 giod #x51c)  ; GPIOD DEN   <- bit 1, Digital Enable for pin PD1
    (bset  2 giog #x51c)  ; GPIOG DEN   <- bit 2, Digital Enable for pin PG2
    (bset  1 giod #x508)  ; GPIOD DR8R  <- bit 1, 8 mA Drive for pin PD1
    (bset  2 giog #x508)  ; GPIOG DR8R  <- bit 2, 8 mA Drive for pin PG2
    (LOS (ash #x0B 17) sysc #x60)) ; RCC <- PWMDIV = 16 (freq = 50MHz/16)

; configure the PWM peripheral
    (write      0 pwm #x40)  ; PWM0CTL   <- 0, disable PWM timer
    (write #x008c pwm #x60)  ; PWM0GENA  <- #x008c, set pin PG2 low on match A
    (write #x080c pwm #x64)  ; PWM0GENB  <- #x080c, set pin PD1 low on match B
    (write   1023 pwm #x50)  ; PWM0LOAD  <- 1023,   period = 0.3 ms (3.05 kHz)
    (write    512 pwm #x58)  ; PWM0CMPA  <-  512,   duty cycle=50% for PG2
    (write    512 pwm #x5c)  ; PWM0CMPB  <-  512,   duty cycle=50% for PD1
    (write      1 pwm #x40)  ; PWM0CTL   <-    1,   enable PWM timer
    (write      3 pwm #x08)) ; PWMENABLE <-    3,   enable PWM0 and PWM1 outputs

; define the set-duty PWM function
  '(define (set-duty v)
    (write (min 1022 (max 0 (- 1023 v))) pwm #x58)
    (write (min 1022 (max 0 (- 1023 v))) pwm #x5c))

; initialize the control data vectors
    (define R_in (make-vector 128 0))
    (define F_in (make-vector 128 0))
    (define E_in (make-vector 128 0))
    (define Cout (make-vector 128 0))
    (define tdat (make-vector 128 0)))

; define the control function used to implement digital controllers
  '(define (control vErr vCtr T)
      (do ((n 0 (+ n 1))
           (tprv 0 (vector-ref tdat n)))
          ((= n 128) #t)
        (pixel n (- 40 (ash (vector-ref R_in n) -4)) 0)
        (pixel n (- 40 (ash (vector-ref F_in n) -4)) 0)
        (wait tprv T)
        (vector-set! tdat n (read timer0 timer-count))
        (vector-set! R_in n (- (read-adc 1) 512))
        (vector-set! F_in n (- (read-adc 2) 512))
        (vector-set! E_in n (- (vector-ref R_in n) (vector-ref F_in n)))
        (vector-set! Cout n
            (- (convolve vErr E_in n)
               (convolve vCtr Cout (- (if (zero? n) (vector-length Cout) n) 1)))))
        (set-duty (+ 512 (vector-ref Cout n)))
        (pixel n (- 40 (ash (vector-ref R_in n) -4)) #x03)
        (pixel n (- 40 (ash (vector-ref F_in n) -4)) #x0f))
    (control vErr vCtr T))

; close the file
(close-output-port p)


Boot File:

In order for the initialization and control files to be loaded automatically on reset they are place in a MCU file named "boot" as shown below. On the next reset the system will initialize and start displaying ADC 1 and 2 data as pixel traces on the OLED, with a 100Hz sampling frequency. To recover the prompt, break out of the infinite scan loop by using ctrl-c. To prevent the boot (in case of a hang) press the Up or Select button while resetting.

; write startup code to the boot file
(let ((p (open-output-file "boot")))
       (load "init-sys")
       (load "init-oled")
       (load "init-adc")
       (load "init-control")
       (load "control")
       (scan 500000))
  (close-output-port p))


PWM Scope:

One can use the above system as a PWM scope provided that the PWM frequency is reduced from its default selected above. In the code below (to be executed at the prompt) the PWM frequency is reduced to its minimum of 12 Hz and the set-duty function is updated accordingly (to bounds of 0 and 65533). You can then connect PWM1 to ADC2 (or PWM0 to ADC1, etc...) with a wire and use (scan 50000) to view the PWM trace on the OLED. The call (scan 50000) tries to sample at 1 KHz which exceeds the maximum speed of the system, leading to a sampling frequency just above 250 Hz (remember to use ctrl-c to exit from scan). The duty cycle can be changed between 0 and 65000 to view different traces. A let-loop is also given below to view the actual interval at which data were sampled, expressed in micro seconds.

; reduce PWM frequency to 12 Hz
(write #xffff pwm #x50)  ; PWM0LOAD  <- #xffff, period = 84 ms (12 Hz)

; redefine set-duty for 12 Hz operation
(define (set-duty v)
  (write (min #xfffd (max 0 (- #xfffe v))) pwm #x58)
  (write (min #xfffd (max 0 (- #xfffe v))) pwm #x5c))

; set PWM duty cycle to approximately 50%
(set-duty 32000)

; scan as fast as possible (ctrl-c to end)
(scan 50000)

; display sampling intervals, in micro-seconds
(let loop ((n 1))
  (if (= n 128) #t
          (* 1000000
            (/ (- (vector-ref tdat (- n 1)) (vector-ref tdat n)) 50000000))))
      (loop (+ n 1)))))

Last updated February 6, 2009