SEGGER RTT (3)
SEGGER RTT (3)
Let’s Build a Trace System
In previous article articles [1][2], I’ve introduced how to use SEGGER RTT as conventional printf method. I’m not fully satisfied with this method as I mentioned in [2]. Because it had several drawbacks- printf() requires SRAM to store format string, which may be a problem is SRAM size is limited
- printf() takes cycles to generate string
- It must not be trivial job to generate decimal value (%d)
printf("idx1=%d, idx2=%d\n", idx1, idx2);
In legacy method, it may require >16bytes and MCU does string formatting. In proposed method, one could only transmit essential values (12bytes)- An ID code which indicates the format string
- ID=1 -> “idx1=%d, idx2=%d\n”
- Parameters
- idx1
- idx2
MCU-Side Program
Our test program is quite easy as shown below. A trace ID macro is defined to facilitate usage. It includes magic number to enable PC-side application to detect the start-of-trace. The payload is loop-count value {i, i<<16}. I put them in an array and invokes SEGGER_RTT_Write() to throw the trace.#define MK_TRACE_HEADER(id) ((0xABCD<<16) | ((id)<<0))
int main(void)
{
unsigned int buf_idx = 0;
while(1) {
unsigned int i;
for(i=0; i<256; i++) {
unsigned int log_buf[3] = {
MK_TRACE_HEADER(0x1),
i,
i<<16,
};
SEGGER_RTT_Write(buf_idx, log_buf, sizeof(log_buf));
nrf_delay_ms(100);
}
}
}
Although you may not be familiar with Nordic’s API, I think that’s quite easy to understand the behavior of the code above. That’s nothing more than a loop keeps throwing logs to SEGGER RTT channel.PC-Side Program
In [2], I already show how to use Python’s telnetlib to build the PC-side application. However, that’s not applicable in this purpose. I tried to use telnetlib to receive the MCU message shown above. However, byte value “0” & “255” are filtered by telnetlib (maybe that’s telnet protocol). So I decided to use SOCKS API:import socket
import array
import struct
def socket_version():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 19021))
while True:
incoming_data = sock.recv(1024)
if incoming_data != None:
trace_parsing(incoming_data)
def main():
os.system('taskkill -im jlink.exe')
os.system(r'start "" "c:\Program Files (x86)\SEGGER\JLink_V510i\JLink.exe" -device nrf51822 -if swd -speed 4000 -autoconnect 1')
time.sleep(1)
socket_version()
Before invokes socket_version(), we added some delay to ensure JLINK’s telnet server is started. In socket_version(), you may find it’s quite easy to use SOCKS library to connect the JLINK’s telnet server. It contains a loop which keeps draining message from SOCKS interface. The message is sent to trace_parsing() to do message parsing:def trace_data_lookup(trace_id):
trace_dict = [
{'len': 1}, # 0x0000
{'len': 2}, # 0x0001
]
return trace_dict[trace_id]['len']
def trace_parsing(incoming_data):
# Incoming data plus existing buffer
buf = trace_parsing.buf + incoming_data
while True:
# Check whether there's sufficient data in buf
if len(buf) < 4:
break
# Get the control word from buffer
ctrl_word = struct.unpack('I', buf[0:4])[0]
magic_word = (ctrl_word >> 16) & 0xFFFF # bit[31:16]
trace_id = (ctrl_word >> 0) & 0xFFFF # bit[15:0]
# Magic word should be 0xABCD, if not matched, omit 1byte here (because the RTT buffer is not 4 byte sync)
if magic_word != 0xABCD:
buf = buf[1:]
continue
# Check whether there's sufficient words in buf (4byte_header + num_word)
trace_len = trace_data_lookup(trace_id)
packet_len = 4 + trace_len*4
if len(buf) >= packet_len:
trace_body = buf[4:packet_len]
emit_trace(trace_id, trace_body)
buf = buf[packet_len:]
else:
break
# Keep uncompleted buffer to static buf
trace_parsing.buf = buf
trace_parsing.buf = ''
The function had a property “buf” which behaves like static variable in C. At first, the incoming message is appended to already existed message. In the loop, it first checks whether the buffer size is greater than 4, which is trace-ID size.Then we use Python’s struct library to interpret the incoming message to extract trace-header. Here’s some interesting synchronization procedure. The header size is 4 byte and we expect 0xABCD in bit[31:16] indicating a valid trace. However, the PC program may start to receive data from byte 0/1/2/3 (with an unknown offset). So the program throws away buffer byte-by-byte until hitting the magic number 0xABCD.
Then trace_data_look() reports how many words in trace. If there’s sufficient data in buffer, then invokes emit_trace() to generate the string:
def emit_trace(trace_id, trace_body):
trace_len = trace_data_lookup(trace_id)
struct_fmt = '%dI' % trace_len
args = struct.unpack(struct_fmt, trace_body)
if trace_id == 0x0000:
print('[TRC 0x0001] arg0=%08X, arg1=%08X' % args)
else:
print('[TRC #%d]' % trace_id),
for i, arg in enumerate(args):
print('arg%d=0x%08X, ' % (i, arg)),
print('')
Screenshot below shows how the program runs. The code above generate different strings according to different traces. For example, one could throw the content of some registers and do bit-unpack in PC-side. This is more efficient in PC-side rather than in target side.The framework proposed here is also quite handy to do arbitrary processing. For example, one may use gathered data to have real time FFT plot.
Conclusion
The pro and cons of this methodPro
- Save code size, data memory size, MCU-cycle in target side. These code can be existed even within product
- Trace code and parsing code in different places. Harder to maintain and coding.
- Need a SEGGER-JLINK to use this method (compare with UART method, which only requires a cheap FTDI chip)
Reference
[1]: SEGGER-RTT (1), http://lihgong.blogspot.com/2016/04/segger-rtt-1.html[2]: SEGGER-RTT (2), http://lihgong.blogspot.com/2016/04/segger-rtt-2.html
[3]: SEGGER’s RTT introduction website, https://www.segger.com/jlink-rtt.html
留言