← Back to list

Get Started with MSP430 MCU and Mecrisp Cross

Tethered Forth and Serial Protocol

circuit4u in The Startup · 2025-04-15 23:01 · 0 claps · 9.4 min read paywalled
#forth #msp430 #embedded-systems
Open on Medium ↗

Get Started with MSP430 MCU and Mecrisp Cross

Tethered Forth and Serial Protocol

With the success of my earlier digital dice project, I picked up another chance to use Mecrisp Cross on a low RAM resource MSP430 MCU (i.e. MSP430G2452). The project is to implement a serial bit protocol, like the Parallax Propeller firmware loader protocol.

1. Mecrisp Cross

Mecrisp Cross is a tethered Forth between the host and the target. The host acts like a code debugger, but with Forth scripting capability. The target is a MSP430 MCU that doesn’t have enough RAM resource to run Forth by itself. The host Forth runs on a STM32F407 and connects to the target over the standard MSP430 2-wire SWDIO/SWCLK debug bus.

With tethered Forth, you can develop and debug MSP430 Forth code interactively on the target (MSP430G2452). And once the code is completed, it is compiled into binary without any overhead from the host’s Forth environment itself.

2. MSP430 MCU

After a MSP430 MCU in this case MSP430G2452 is selected, some essential information comes from its MCU datasheet:

2.1 terminal functions table

For example, not all GPIO pin can serve as ADC10 analog input, similarly Timer0 capture can only happen on certain pins for example. These will determine the pinouts of your schematic design.

2.2 Various memory address of registers and interrupt vector addresses

Mecrisp Forth/Cross already has these register addresses defined as constants. Fortunately a lot of MSP430 MCUs adopt the same address constants. So these tables are almost universal across different MSP430 MCUs.

The bit fields for each register is detailed inside the user manual.

For example, TACTL register has a lot of bit fields. Here is an example table in “Timer_A” chapter of the user manual:

The next important question is how to set these register fields in order to use the timer hardware module. The c-code examples in the zip file (SLAC467) are very helpful. It has plenty of examples showing these bit fields configured in various timer applications.

Note that C code also defines a bunch of address constants in a “msp430.h” file. It’s helpful to find the corresponding “msp430g2452.h” file for our MCU.

The next step is to turn the knowledge from relevant C examples into Forth code.

3. Mecrisp Cross Forth Code

The following is a very verbose way of defining all the bit fields for TACTLand BCSCTL3 registers.

host
new
+jtag
target

 : TA0CTL_MC<< ( x -- ) 4 lshift ; \ Timer A mode control 1
      0 constant MC_0      \ Timer A mode control: 0 - Stop  
      1 constant MC_1      \ Timer A mode control: 1 - Up to CCR0  
      2 constant MC_2      \ Timer A mode control: 2 - Continous up  
      3 constant MC_3      \ Timer A mode control: 3 - Up/Down  
 : TA0CTL_ID<< ( x -- ) 6 lshift ; \ Timer A clock input divider 1
      0 constant ID_0      \ Timer A input divider: 0 - /1  
      1 constant ID_1      \ Timer A input divider: 1 - /2  
      2 constant ID_2      \ Timer A input divider: 2 - /4  
      3 constant ID_3      \ Timer A input divider: 3 - /8  
 : TA0CTL_TASSEL<< ( x -- ) 8 lshift ; \ Timer A clock source select 1
      0 constant TASSEL_0      \ Timer A clock source select: 0 - TACLK  
      1 constant TASSEL_1      \ Timer A clock source select: 1 - ACLK  
      2 constant TASSEL_2      \ Timer A clock source select: 2 - SMCLK  
      3 constant TASSEL_3      \ Timer A clock source select: 3 - INCLK
 : BCSCTL3_LFXT1S<< ( x -- ) 4 lshift ; \ Mode 0 for LFXT1 (XTS = 0)
      0 constant LFXT1S_0      \ Mode 0 for LFXT1 : Normal operation  
      1 constant LFXT1S_1      \ Mode 1 for LFXT1 : Reserved  
      2 constant LFXT1S_2      \ Mode 2 for LFXT1 : VLO  
      3 constant LFXT1S_3      \ Mode 3 for LFXT1 : Digital input signal

 : configure ( -- )
 \ timer clock                    \ LFXT1 feeds ACLK
  LFXT1S_2 BCSCTL3_LFXT1S<<       \ VLO = 12 kHz  (or LFXT1S_0 = 32768-Hz crystal on MSP430G2201)
              BCSCTL3 cbis!       \ note: XTS = 1 is not supported in MSP430G2201 devices
 \ timer
   MC_2 TA0CTL_MC<<
    ID_0 TA0CTL_ID<<
     TASSEL_1 TA0CTL_TASSEL<<
              + + TACTL bis!     \ TA0CTL @ bin. 0000000100100001 ok.    
 ;

 : main ( -- ) cr
   configure
   0 TAR !        \ clear counter
   TAR @ . cr     \ read free running counter
   TAR @ . cr
   TAR @ . cr
   TAR @ . cr
   TAR @ . cr
   TAR @ . cr
   TAR @ . cr
   TAR @ . cr
 ;

 main

lshift, cbis! and cbic! are used to manipulate the bit fields of the MSP430 registers. Mecrisp Cross dictionary contains many other Forth primitives on the target device. Once you are familiar with these constants, the code doesn’t have to be that verbose.

In the last step, the complete binary code, both “main” and “IRQ” functions are stored in the vector table like this:

 host           \ leave target mode, enter host mode
  $FFE4 vector P1.3-int-handler        \ push button interrupt vector
  $FFFE vector main crosscompile flashtarget -jtag run     \ crosscompile the code above and flash it to the target
\ hexdump          \ print a neat hexdump of the cross compiled binary
\ t-listing          \ t-listing shows what the cross compiler did
\ disimage      \ disamige produces a disassembly listing of the target binary

See example interrupt vector functions.

hexdumpcan produce bin file to be programmed into MSP430 flash with its UNIFLASH directly, especially for mass production.

4. Parallax Propeller Serial Protocol

The goal of the project is to implement a serial data protocol like the one used by Parallax Propeller MCU to flash its firmware.

It is a time slot based bit protocol. The BIT_ZERO is represented by 2T time slot, BIT_ONE is represented by 1T time slot. Once triggered by a falling edge, the receiver samples at 1.5T time slot to determine whether it’s a BIT_ZERO or BIT_ONE.

The nice thing about this protocol is that 1T or 2T aren’t predetermined. To establish the timing, the sender will send two calibration pulses one with 1T time slot and the other with 2T time slot, before sending the following data bits.

A typical RS232 serial protocol can be hijacked to implement this serial protocol. Because sending 0xF9 on a serial bus will appear as

0_1_0_0_1_1_1_1_1

Note that the first zero of 1T is the start bit, and the second zeros of 2T is due to RS232 sending LSB first of byte 0xF9.

Similarly to send BIT_ZERO, one sends byte 0xFF over the serial bus; to send BIT_ONE, one sends byte 0xFE instead.

Here is FSM code in Forth to detect BIT_ZERO and BIT_ONE with the help of MSP430 timer capture/compare functions.


new
+jtag
target

$166 constant TACCTL2
$176 constant TACCR2

0 constant IDLE
1 constant HL_E1   \ to derive 1t
2 constant LL_E2
3 constant HL_E2   \ to derive 2t
4 constant BIT_LL
5 constant BIT_HL  \ magic byte 0x78
6 constant TX_BIT
7 constant EXIT    \ failed

0 variable state 
0 variable t_LL_E1
0 variable t_HL_E1
0 variable t_LL_E2
0 variable t_HL_E2
0 variable t_1
0 variable t_1p5
0 variable bit_cnt
0 variable byte_in
0 variable byte_cnt
0 variable byte_out

: fsm
state @ CASE 
  IDLE OF   
     taccr0 @ dup t_LL_E1 !   \ save falling edge time from TAR
     $4910 tacctl0 !          \ rising edge+cap+int
     60000 + taccr2 !         \ xms timeout
     16 tacctl2 !             \ enable int
     HL_E1 state !  
  ENDOF
  HL_E1 OF
     taccr0 @ t_HL_E1 !  \ save rising edge time from TAR
     $8910 tacctl0 !     \ set to capture next falling edge
     LL_E2 state !  
  ENDOF
  LL_E2 OF
     taccr0 @ t_LL_E2 !  \ save falling edge time from TAR
     $4910 tacctl0 !     \ rising edge cap int
     HL_E2 state !       \ set state to LL_E2
  ENDOF
  HL_E2 OF
     taccr0 @ t_HL_E2 !                                \ save rising edge time from TAR
     t_HL_E1 @ t_LL_E1 @ - t_1 !                       \ save 1t time    
     t_HL_E2 @ t_LL_E2 @ - t_HL_E1 @ t_LL_E1 @ -  2dup \ calculate time between edges
     - 0< if 
        EXIT state !                                   \ calibration failed; jump to EXIT    
     else
        + 2/ t_1p5 !                                   \ set to compare at 1.5t
        8 bit_cnt !                                    \ set bit counter to 7 
        0 byte_in !
        0 byte_cnt !
        $8910 tacctl0 !                                \ set to capture next falling edge
        BIT_LL state !  
        tar @ 60000 + taccr2 !                         \ kick timeout
     then
  ENDOF
  BIT_LL OF
    t_1p5 @ taccr0 +! \ 1.5T later
    $0010 tacctl0 !   \ switch to comp mode
    tar @ 60000 + taccr2 !                         \ kick timeout
    BIT_HL state !  
  ENDOF
  BIT_HL OF
    1 10 lshift tacctl0 bit@  if \ read scci high
       byte_in @ shl 1+ byte_in !
    else 
       byte_in @ shl byte_in !
    then

    bit_cnt @ 1- dup
    0= if \ byte received
      drop 
      byte_cnt @ case 
        0 of \ header byte
         byte_in @ $78 = if       \ compare with 0x78
           tar @ 60000 + taccr2 ! \ kick timeout
           0 byte_in !
           $8910 tacctl0 !        \ set to capture next falling edge
           BIT_LL state !  
           8 bit_cnt !            \ set bit counter to 8 for second byte
         then
        ENDOF
        1 of                           \ cmd byte
          1 7 lshift byte_in bit@  if  \ MSB HIGH         
            TX_BIT state !
            tar @ 60000 + taccr2 !     \ kick timeout
            103 byte_out !             \ test value only
            byte_out @ shl  byte_out ! \ add zero as start bit
            11 bit_cnt !               \ preset + start bit + 8-bit + end
            tar @ 200 + taccr1 !       \ kickstart first int
            1 4 lshift tacctl1 bis!    \ int
          else
            byte_in @ p1out c!         \ set 5 bits: p2.7(H2), p2.6(H1), p1.7(S3), p1.6(S2), p1.5(S1);
          then
        ENDOF
      ENDCASE
      byte_cnt @ 1+ byte_cnt !
   else
     bit_cnt !
     $8910 tacctl0 ! \ set to capture next falling edge
     BIT_LL state !  
    then
  ENDOF
  TX_BIT OF
    t_1 @ taccr1 +!
    1 6 lshift p1out cxor!    \ test signal 

    bit_cnt @ 1- dup
    0= if
       1 2 lshift tacctl1 ! \ set
    else
       bit_cnt !
       1 byte_out bit@ if           
         1 7 lshift tacctl1 bic!      \ outmod 001 (set)       
       else 
         1 7 lshift tacctl1 bis!      \ outmod 101 (reset)
       then
       byte_out @ shr byte_out !     
    then   
  ENDOF
  EXIT OF    
    IDLE state !         \ set next state to IDLE
    $8910 tacctl0 !      \  falling edge sync. cap int
    0 tacctl2 !          \ clear timeout
    1 5 lshift tacctl1 ! \ outmod 001 (set)
    lpm1                 \ sleep
  ENDOF

ENDCASE
;

: ser_tx_init
  1 2 lshift p1dir cbis!  \ p1.2 output
  1 2 lshift p1sel cbis!  \ p1.2 timer function
  1 2 lshift tacctl1 bis! \ output high
  1 5 lshift tacctl1 !    \ outmod 001 (set)
;

: init                      \ signal on negative input; reference on positive input
   16MHz
   1 p1dir cbis!            \ p1.0 output
   1 6 lshift p1dir cbis!   \ p1.6 output
   1 1 lshift p1ren cbis!   \ p1.1 timer function; pull-up
   1 1 lshift p1dir cbic!   \ p1.1 as input
   1 1 lshift p1out cbis!
   1 1 lshift p1sel cbis!
   $220 TACTL  !            \ smclk, cont. Mode
   ser_tx_init
   EXIT state !
   fsm
;

: timer_int   
  wakeup  \ wakeup from sleep
  fsm
;

: timer_int2
  taiv @ case
    2 OF \ cc1
      fsm
    ENDOF
    4 OF \ cc2
      EXIT state !
      fsm
     1 6 lshift p1out cxor! 
    ENDOF    
  endcase
;

: main 
  init
  eint
;

host               \ leave target mode, enter host mode  
$fff2 vector timer_int                 \ timer t0 interrupt vector
$fff0 vector timer_int2        \ timer t1 interrupt vector
$FFFE vector main  
crosscompile flashtarget -jtag run     \ crosscompile the code above and flash it to the targe

The rest of the functions are listed below. The FSM function is triggered by timer (CC0) interrupt on each signal transition, either rising or falling edge. Time out (CC2) jumps FSM to the beginning if something goes wrong.


: init                      \ signal on negative input; reference on positive input
   16MHz
   1 p1dir cbis!            \ p1.0 output
   1 6 lshift p1dir cbis!   \ p1.6 output
   1 1 lshift p1ren cbis!   \ p1.1 timer function; pull-up
   1 1 lshift p1dir cbic!   \ p1.1 as input
   1 1 lshift p1out cbis!
   1 1 lshift p1sel cbis!
   $220 TACTL  !            \ smclk, cont. Mode
   ser_tx_init
   EXIT state !
   fsm
;

: timer_int   
  wakeup  \ wakeup from sleep
  fsm
;

: timer_int2
  taiv @ case
    2 OF \ cc1
      fsm
    ENDOF
    4 OF \ cc2
      EXIT state !
      fsm
     1 6 lshift p1out cxor! 
    ENDOF    
  endcase
;

: main 
  init
  eint
;

host               \ leave target mode, enter host mode  
$fff2 vector timer_int                 \ timer t0 interrupt vector
$fff0 vector timer_int2        \ timer t1 interrupt vector
$FFFE vector main  
crosscompile flashtarget -jtag run     \ crosscompile the code above and flash it to the targe

Here is the sequence of serial bytes to send over P1.1 (MSP430 RX)

0xF9, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,0xFE,0xFE, …

corresponds to:

calibration byte, BIT0, BIT1, BIT1, BIT1, BIT1,BIT0, BIT0, BIT0, …

The magic number 0x78 is used as a header preceding the rest of the payload bytes.

Currently the sender can use 115200baud rate serial port to send these data. For slower baud rate, when MCLKat 16MHz is faster enough to measure 1.5T with enough counts.

To serial TX data back is a bit tricky. Notice that in order to send out 8-bit, the timer interrupt needs to be triggered 11 times.

The outmode of set/reset is happening on the timer compare next interrupt (taccr1 == tar). So it’s a bit like writing Verilog sequential code, where things happen at the next clock cycle.

1 byte_out bit@ if           
  1 7 lshift tacctl1 bic!      \ outmod 001 (set)       
else 
  1 7 lshift tacctl1 bis!      \ outmod 101 (reset)
then

Here is the confusing c-code snippet.

 else
    {
      CCTL0 |=  OUTMOD2;                    // TX Space
      if (RXTXData & 0x01)
      CCTL0 &= ~ OUTMOD2;                   // TX Mark
      RXTXData = RXTXData >> 1;
      BitCnt --;
    }

But as long as you realize CCTL0 bit setting only takes effect at next timer interrupt, these set/reset steps aren’t worrying.


메타데이터
post_id
62eef912a2bf
slug
get-started-with-msp430-mcu-and-mecrisp-cross-62eef912a2bf
url
https://medium.com/@circuit4us/get-started-with-msp430-mcu-and-mecrisp-cross-62eef912a2bf
canonical_url
https://medium.com/@circuit4us/get-started-with-msp430-mcu-and-mecrisp-cross-62eef912a2bf
author_url
https://medium.com/@circuit4us
status
ok
fetched_at
2026-07-20 07:29:46