LinkServer Scripting, and how to Recover MCUs with a Script

The MCU-Link is an inexpensive CMSIS-DAP debug probe from NXP. It can be used as a GDB server debug probe, and as such it includes scripting support. This scripting can be very useful in some cases where the MCU cannot be accessed by a normal debug session. This happens for example if students are not pay attention what binary they flash to which device, causing an MCU to potentially get ‘bricked’.

NXP MCU-Link debug probe connected to Sumo Robot

Outline

Using a SWD debug connection, it is possible that the debug probe might not be fast enough to regain access to the MCU. By default using the SWD debug protocol, the debug probe tries to reset the MCU, and then halt it. But in cases where the MCU immediately disables the debug block, enters a deep low power mode or anything else disabling access over debug, that device can be ‘bricked’. In the past and for Kinetis devices, I have used a special program provided by PEMICRO (see Recovering Cortex-M Microcontroller with a Power Glitch). But that approach required a P&E debug probe plus and only applies to Kinetis devices.

In this article, I give a short overview how to use scripts with the MCU-Link and Linkserver. Additionally I provide a script I’m using to recover MCU’s which are otherwise not accessible with a normal debug session. If flash security (see How (not) to Secure my Microcontroller) has been enabled, then you might be (officially) out of luck. But otherwise a small recovery script might you get you out.

The idea is to run a script with the debug probe, to catch the MCU right after it is coming out of reset. I’m using here the NXP MCU-Link debug probe, which can be used with an IDE like Eclipse (MCUXpresso IDE) or standalone in command-line mode using the LinkServer software.

LinkServer Scripts

The MCUXpresso IDE Users guide provides documentation about the scripting features:

MCUXpresso IDE User Manual about LinkServer Scripts

Example script files can be found inside the following MCUXpresso IDE directory:

<install dir>/ide/binaries/Scripts

Scripts

The above scripts are used by the LinkServer, e.g. when launching a debug session from Eclipse. I have used them as examples and inspiration for my scripts.

Help Command

Another good source of information is the ‘help’ command within the command line of the standalone LinkServer. Inside the ‘binaries’ sub-folder of the LinkServer installation is a ‘redlinkserv’ executable which has a ‘help’ command:

Launch the tool with

redlinkserv --commandline
Launching Redlinkserv

In the command line mode the help

redlink>help
********** Probe related functions ***********
PROBELIST : Enumerates and returns an indexed list of known probe types
PROBENUM : Returns the number of probes attached
PROBEOPENBYINDEX <ProbeIndex> [<"FILENAME">] : Opens the probe associated with ProbeIndex
  FILENAME is text of <key=value> pairs used for internal configuration
 PROBEOPENBYSERIAL <"SerialNumber"> [<"FILENAME">]: Opens the probe associated with SerialNumber
PROBECLOSEBYINDEX <ProbeIndex>: Closes the probe associated with ProbeIndex
PROBECLOSEBYSERIAL <"SerialNumber">: Closes the probe associated with SerialNumber
PROBEFIRSTFOUND : Returns the THIS ProbeIndex or index of the first probe in the
  enumerated list
PROBETIME <ProbeIndex> : Returns elapsed time from firmware boot, if supported
PROBESTATUS [<ProbeIndex>]: Returns an indexed list summary of the status of the
probes connected to the system
PROBEVERSION <ProbeIndex>: Returns version information about probe firmware
PROBEISOPEN <ProbeIndex>: Returns TRUE or FALSE
PROBEHASJTAG <ProbeIndex>: Returns TRUE or FALSE
PROBEHASSWD <ProbeIndex>: Returns TRUE or FALSE
PROBEHASSWV <ProbeIndex>: Returns TRUE or FALSE
PROBEHASETM <ProbeIndex>: Returns TRUE or FALSE

********** Core/TAP related functions ******
CORECONFIG {[THIS] | [<ProbeIndex>]}: Queries the scan chain configuration
CORESCONFIGURED <ProbeIndex>: Returns TRUE or FALSE
APLIMIT {[THIS] | [<ProbeIndex>]}:  <APIndex>:  Limit the AP Query (set once)
APLIST  {[THIS] | [<ProbeIndex>]}: [<APLimit>]: Detailed list of APs
  connected to the specified probe. APLimit restricts queries to AP index.
CORELIST {[THIS] | [<ProbeIndex>]}: [<APLimit>]: Detailed list of APs/Cores
  connected to the specified probe. APLimit restricts queries to AP index.
COREREADID {[THIS] | [<ProbeIndex> <CoreIndex>]}: Returns the DpID
DEBUGMAILBOXREQ {[THIS] | [<ProbeIndex> <APIndex>]} <Request>: Debug Mailbox Request

********** Wire related functions **********
WIRESWDCONNECT {[THIS] | [<ProbeIndex>]}: Configures the wire for SWD and
  returns the DpID
WIREJTAGCONNECT {[THIS] | [<ProbeIndex>]}: Configures the wire for JTAG
WIREDISCONNECT {[THIS] | [<ProbeIndex>]}: Closes the wire connection (SWD/JTAG)
WIREISPRESET {[THIS] | [<ProbeIndex>]}: Resets an LPC part into the ISP
  bootloader
WIREBOOTCONFIGSET {[THIS] | [<ProbeIndex>]} <"DATA">: Stores boot configuration data
  that will be automatically applied during subsequent reset commands.
  DATA is a string with up to 4 characters describing how each ISP_CTRL[3..0] pin
  should be handled: '0' (= drive low), '1' (= drive high), 'x' (= do not drive)
WIREBOOTCONFIGGET {[THIS] | [<ProbeIndex>]}: Returns previously stored configuration data
WIREBOOTCONFIGREAD {[THIS] | [<ProbeIndex>]}: Returns the current state of ISP_CTRL[3:0] pins
WIREBOOTCONFIGAPPLY {[THIS] | [<ProbeIndex>]} <1/0>: Immediately starts/stops driving
  the ISP_CTRL pins based on previously stored boot configuration data
WIRETIMEDRESET <ProbeIndex> <ms>: Asserts (Low) reset for ms milliseconds and
  returns the end state of the wire
WIREHOLDRESET <ProbeIndex> <State> : Asserts/Releases (Low/High) reset and
  returns the end state of the wire
WIRESETSPEED <ProbeIndex> <Hz>: Requests a particular wire speed in Hz
WIREGETSPEED <ProbeIndex> : Returns the current wire speed
WIRESETIDLECYCLES <ProbeIndex> <Cycles>: Sets the number of idle cycles between
  debug transactions
WIREGETIDLECYCLES <ProbeIndex> : Returns the current number of debug idle cycles
WIREISCONNECTED <ProbeIndex>: >: Returns TRUE or FALSE if WIRESWDCONNECT or
  WIREJTAGCONNECT is complete
WIREGETPROTOCOL <ProbeIndex>: Returns SWD or JTAG
SELECTPROBECORE <ProbeIndex> <CoreIndex> : Sets the THIS parameter Probe/Core
  pair
THIS : Displays the current Probe, Core pair

********** Cortex-M related functions ******
CMINITAPDP {[THIS] | [<ProbeIndex> <CoreIndex>]}: Initialize a CMx core ready
  for debug connections
CMUNINITAPDP {[THIS] | [<ProbeIndex> <CoreIndex>]}: UnInitialize a CMx core
  (de-assert debug and system power-up)
CMWRITEDP {[THIS] | [<ProbeIndex> <CoreIndex>]} <REG> <DATA>: Returns zero on
  success
CMWRITEAP {[THIS] | [<ProbeIndex> <CoreIndex>]} <REG> <DATA>: Returns zero on
  success
CMREADDP {[THIS] | [<ProbeIndex> <CoreIndex>]} <REG>: Returns data
CMREADAP {[THIS] | [<ProbeIndex> <CoreIndex>]} <REG>: Returns data (handles
  RDBUF on AP reads)
CMCLEARERRORS {[THIS] | [<ProbeIndex> <CoreIndex>]}
CMHALT {[THIS] | [<ProbeIndex> <CoreIndex>]}
CMRUN {[THIS] | [<ProbeIndex> <CoreIndex>]}
CMSTEP {[THIS] | [<ProbeIndex> <CoreIndex>]}
CMREGS {[THIS] | [<ProbeIndex> <CoreIndex>]}
CMDEBUGSTATUS {[THIS] | [<ProbeIndex> <CoreIndex>]}
CMWRITEREG {[THIS] | [<ProbeIndex> <CoreIndex>]} <RegNumber> <Value>
CMREADREG {[THIS] | [<ProbeIndex> <CoreIndex>]} <RegNumber>
CMWATCHLIST {[THIS] | [<ProbeIndex> <CoreIndex>]}
CMWATCHSET {[THIS] | [<ProbeIndex> <CoreIndex>]} <DWTIndex> <Address> [<RW|R|W>]
CMWATCHCLEAR {[THIS] | [<ProbeIndex> <CoreIndex>]} <DWTIndex>
CMBREAKLIST {[THIS] | [<ProbeIndex> <CoreIndex>]} : List the FPB breakpoints
CMBREAKSET {[THIS] | [<ProbeIndex> <CoreIndex>]} <Address> : Set an FPB
CMBREAKCLEAR {[THIS] | [<ProbeIndex> <CoreIndex>]} [<Address>] : Clear an FPB
CMSYSRESETREQ {[THIS] | [<ProbeIndex> <CoreIndex>]} : System reset request
CMVECTRESETREQ {[THIS] | [<ProbeIndex> <CoreIndex>]} : Core reset request
CMRESETVECTORCATCHSET {[THIS] | [<ProbeIndex> <CoreIndex>]} : Enable reset
vector catch
CMRESETVECTORCATCHCLEAR {[THIS] | [<ProbeIndex> <CoreIndex>]} : Disable reset
  vector catch

********** BASIC-like Commands *************
PEEK8  {[THIS] | [<ProbeIndex> <CoreIndex>]} <Address>
PEEK16 {[THIS] | [<ProbeIndex> <CoreIndex>]} <Address>
PEEK32 {[THIS] | [<ProbeIndex> <CoreIndex>]} <Address>
POKE8  {[THIS] | [<ProbeIndex> <CoreIndex>]} <Address> <Data>
POKE16 {[THIS] | [<ProbeIndex> <CoreIndex>]} <Address> <Data>
POKE32 {[THIS] | [<ProbeIndex> <CoreIndex>]} <Address> <Data>
QPOKE8  {[THIS] | [<ProbeIndex> <CoreIndex>]} <Address> <Data>
QPOKE16 {[THIS] | [<ProbeIndex> <CoreIndex>]} <Address> <Data>
QPOKE32 {[THIS] | [<ProbeIndex> <CoreIndex>]} <Address> <Data>
QSTARTTRANSFERS {[THIS] | [<ProbeIndex> <CoreIndex>]} <NumReads>
MEMDUMP {[THIS] | [<ProbeIndex> <CoreIndex>]} <Byte Address> <Length>
MEMLOAD {[THIS] | [<ProbeIndex> <CoreIndex>]} <FileName> <Byte Address> <Length
  Limit>  Loads binary file data to memory
MEMSAVE {[THIS] | [<ProbeIndex> <CoreIndex>]} <FileName> <Byte Address> <Length>
  Saves memory to binary file
EXIT: Exit the server
PRINT "TEXT"[;[~]Variable | Constant]: Print statement. Prints quoted text
  and/or value of an internal variable (a%% - z%%), or constant integer
  expression in decimal, or hexadecimal[~] format
TIME : Returns an incrementing centisecond count from the host
TIMEMS : Returns an incrementing millisecond count from the host
WAIT <msec> : Wait for the number of milliseconds before proceding
LIST: Lists a loaded script
NEW: Erases a loaded script from memory
RENUMBER <Delta>: Renumber script lines with Delta increment (default is 10)
LOAD <"FILENAME">: Loads a script from the current, absolute, or relative directory
SAVE <"FILENAME">: Saves a script to the current, absolute, or relative directory

********** BASIC-like Keywords For Script Only **
GOTO <LineNumber>
IF <relation> THEN <statement> [ELSE <statement>]
REPEAT : Start of a repeat block
UNTIL <relation> : End with condition of repeat block
BREAKREPEATTO <LineNumber> : Premature end of a repeat loop
GOSUB <LineNumber>
RETURN

💡 Some commands like the LOAD one are used in ‘interactive’ mode. I have not found a way to use ‘load’ in script with a relative path. It works with an absolute path, but that’s not what I want. If I find a solution, I’ll post an update.Running Scripts

Script files can be referenced and called from a launch configuration.

Launch Configuration with calling connect script

I usually place the ‘main’ script file in the root directory of the project as shown above.

I can print variables or text from scripts, for example:

10 print "hello from my script"

and it will show up in the console view as below:

Script output

This is useful to ‘debugging’ scripts. To use a script in a (relative) sub-folder of the project, the path has to start with a ‘\’ or ‘/’:

Script in project sub-folder

Using LinkServer

Another way is to run scripts from the LinkServer program, which is a command-line utility. Inside the installation there is a utility I can use in interactive mode:

binaries\redlinkserv --commandline

then load the script and run it:

load "script.scp"
run

If the script has been edited outside, use the ‘new’ and ‘load’ again:

new
load "script.scp"
run

To exit the session:

exit

Another way is to run a gdb server session listening on a port:

linkserver gdbserver --redlink-telnet-port 12345 LPC845:LPC845

Note that here I have to provide a device name/board. Then I connect to the port with a telnet program (e.g. putty or nc) and use the commands as above.

Telnet Session with PuTTY

If using PuTTY, enable the Terminal setting ‘Implicit CR in every LF’.

Unlock Script

Below is a script which tries to unlock a ‘bricked’ MCU: first it searches for an attached debug probe. Then, inside a loop, it tries connect and halt the device:

  10 PRINT "*****************"
  11 PRINT "* Unlock Script *"
  12 PRINT "*****************"
  20 PRINT "search probe ..."
  30 PROBELIST
  40 p% = PROBEFIRSTFOUND
  50 PROBEOPENBYINDEX p%
 100 PRINT "connnecting..."
 110 w% = WIRESWDCONNECT p%
 111 REM PRINT "w: "; w%
 112 REM IF w% == 0 THEN GOTO 1000
 200 i% = 0
 210 REPEAT
 230   w% = WIRESWDCONNECT p%
 231   REM PRINT "w: "; ~w%
 240   u% = SelectProbeCore p% 0
 241   REM PRINT "u: "; u%
 250   v% = CMINITAPDP this
 251   REM PRINT "v: "; v%
 260   h% = CMHALT this
 261   REM PRINT "halt: "; h%
 270   i% = i% + 1
 280 UNTIL i% >= 10
 300 PRINT "reached max retry: "; i%
 310 END
 
 1000 PRINT "failed"
 1010 END

I have not found a good way to stop the script after it is able to halt to the target. To keep it simple, it just tries to halt the MCU until the max retry count is reached: change that value i% if you need it to run for a longer time.

Running the Unlock Script

While the script is running, it tries to halt the MCU. So during this time, I repower or reset the MCU many times, to give the script a chance to halt the MCU. Below is the output where it was able to halt it after 10 retries and received the DpID and APID:

*****************
* Unlock Script *
*****************
search probe ...
Index = 1
Manufacturer = NXP Semiconductors
Description = LPC11U3x CMSIS-DAP v1.0.7
Serial Number = 1103F02B
VID:PID = 1FC9:0132
Path = \\?\hid#vid_1fc9&pid_0132&mi_00#7&35c6d4a8&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}

Probe Handle 1 Open
trying to connnect to target...
Error: Wire Ack Fault - target connected?
Iteration 0
Error: Wire Ack Fault - target connected?
Error: Wire not connected
Error: Wire not connected
Iteration 1
Error: Wire Ack Fault - target connected?
Error: Wire not connected
Error: Wire not connected
Iteration 2
Error: Wire Ack Fault - target connected?
Error: Wire not connected
Error: Wire not connected
Iteration 3
Error: Wire Ack Fault - target connected?
Error: Wire not connected
Error: Wire not connected
Iteration 4
Error: Wire Ack Fault - target connected?
Error: Wire not connected
Error: Wire not connected
Iteration 5
Error: Wire Ack Fault - target connected?
Error: Wire not connected
Error: Wire not connected
Iteration 6
Error: Wire Ack Fault - target connected?
Error: Wire not connected
Error: Wire not connected
Iteration 7
Error: Wire Ack Fault - target connected?
Error: Wire not connected
Error: Wire not connected
Iteration 8
Error: Wire Ack Fault - target connected?
Error: Wire not connected
Error: Wire not connected
Iteration 9
DpID = 0BC11477
APID = 0x04770031
reached max retry: 10

At this state showing DpID and APID (ID 0x04770031 is for an ARM Cortex-M0+), the MCU is halted and I can erase it or load a new firmware. Success! 🙂

Summary

I have now scripting capabilities available with the LinkServer (LPC-Link, MCU-Link and MCU-Link Pro) debug probes. This gives me an inexpensive hardware tool to recover MCUs in some cases, and you might find the scripting capabilities useful for your special debug needs too. The LinkServer scripting language is very BASIC (pun intended), gives you access to the debug hardware and low level stuff, and with some help of existing scripts and forum questions I got it up and running. As for the unlocking script, I have it running ‘semi-automatic’ in the ‘interactive’ mode which works fine. Ideally I would run the script from another script, and if I find out how to do this, I will update this article.

The recovery approach I’m using here is to run a script repeatedly to gain access to the device after reset or power-on. It seems to work fine at least with the cases I had. But for a higher speed it might be necessary to run the code on the debug probe itself. This is maybe something I will look at in the future.

Happy recovering 🙂

Links

2 thoughts on “LinkServer Scripting, and how to Recover MCUs with a Script

What do you think?

This site uses Akismet to reduce spam. Learn how your comment data is processed.