Raspberry Pi: Difference between revisions
Jump to navigation
Jump to search
[unchecked revision] | [unchecked revision] |
Content deleted Content added
No edit summary |
|||
Line 48: | Line 48: | ||
Now you are ready to start. |
Now you are ready to start. |
||
==Tutorials== |
==Tutorials and examples== |
||
# [http://www.cl.cam.ac.uk/freshers/raspberrypi/tutorials/os/ Tutorial in assembler (University of Cambridge)] |
# [http://www.cl.cam.ac.uk/freshers/raspberrypi/tutorials/os/ Tutorial in assembler (University of Cambridge)] |
||
# [[ARM_RaspberryPi_Tutorial_C|Tutorial in C]] |
# [[ARM_RaspberryPi_Tutorial_C|Tutorial in C]] |
||
Line 73: | Line 73: | ||
==Boot-from-serial kernel== |
==Boot-from-serial kernel== |
||
The RPi boots the kernel directly form SD card and only from SD card. There is no other option. While devloping this becomes tiresome since one has to constantly swap the SD card from the RPi to a SD card reader and back. Writing the kernel to the SD card over and over also wears out the card. Plus the SD card slot is somewhat fragile, several people have reported that they broke it accidentally. So what can we do abou that? |
The RPi boots the kernel directly form SD card and only from SD card. There is no other option. While devloping this becomes tiresome since one has to constantly swap the SD card from the RPi to a SD card reader and back. Writing the kernel to the SD card over and over also wears out the card. Plus the SD card slot is somewhat fragile, several people have reported that they broke it accidentally. Overall not an ideal solution. So what can we do abou that? |
||
I've written a small bootloader named [[https://github.com/mrvn/raspbootin|Raspbootin]] based on the Tutorial in C above that loads the real kernel from the serial port. Raspbootin is acompanied by Raspbootcom ([[https://github.com/mrvn/raspbootin|same repository]]) that acts as a boot server and terminal program. Using the two I only need to reboot my RPi to get it to boot the latest kernel. That makes testing both faster and saver for the hardware. |
|||
Above we have seen how to get into C/C++ code at boot and how to read from and write to the serial port. We can use that to download code over the serial port and then execute that. We will call that kernel, or bootloader if you will, Raspbootin (pronounced Rasputin). Before you start editing files make a copy of the echo-kernel you have so far. We will later boot the echo-kernel over the serial console to test Raspbootin. |
|||
Raspbootin is completely transparent for your kernel. It preserves the r0, r1 and r2 registers and ATAGs placed into memory by the GPU for your kernel. So weather you boot your kernel directly from SD Card or with Raspbootin via serial port makes no difference to your code. |
|||
To make Raspbootin work we need a bootloader on the SD card but also an app on another system that then uploads the kernel over the serial port. Those two need to communicate and for that we have a boot protocol. |
|||
=== |
===Raspbootin serial protocol=== |
||
You don't have to care about this unless you want to write your own boot server. |
|||
The boot protocol for Raspbootin is rather simple. Raspbootin first sends 3 breaks (\x03) over the serial line to signal that it is ready to recieve a kernel. It then expects the size of the kernel as uint32_t in little endian byte order. After the size it replies with "OK" if the size is acceptable or "SE" if it is too large for it to handle. After "OK" it expects size many bytes representing the kernel. That's it. |
|||
The boot protocol for Raspbootin is rather simple. Raspbootin first sends 3 breaks (\x03) over the serial line to signal that it is ready to receive a kernel. It then expects the size of the kernel as uint32_t in little endian byte order. After the size it replies with "OK" if the size is acceptable or "SE" if it is too large for it to handle. After "OK" it expects size many bytes representing the kernel. That's it. |
|||
===Raspbootin=== |
|||
The bootloader will be called Raspbootin (pronounced Rasputin) and is verry similar to what we have already. None the less some changes need to be made. The problem is that any RPi kernel expects to be loaded at 0x8000 and started there. That also holds for Raspbootin itself. Loading a new kernel to 0x8000 would overwrite Raspbootin. But loading the new kernel somewhere else won't work either. So we have to move Raspbootin out of the way first before loading the new kernel. |
|||
====boot.S==== |
|||
<source lang=asm> |
|||
// To keep this in the first portion of the binary. |
|||
.section ".text.boot" |
|||
// Make Start global. |
|||
.globl Start |
|||
// Entry point for the kernel. |
|||
// r15 -> should begin execution at 0x8000. |
|||
// r0 -> 0x00000000 |
|||
// r1 -> 0x00000C42 |
|||
// r2 -> 0x00000100 - start of ATAGS |
|||
// preserve these registers as argument for kernel_main |
|||
Start: |
|||
// Setup the stack. |
|||
mov sp, #0x8000 |
|||
// we're loaded at 0x8000, relocate to _start. |
|||
.relocate: |
|||
// copy from r3 to r4. |
|||
mov r3, #0x8000 |
|||
ldr r4, =_start |
|||
ldr r9, =_data_end |
|||
1: |
|||
// Load multiple from r3, and store at r4. |
|||
ldmia r3!, {r5-r8} |
|||
stmia r4!, {r5-r8} |
|||
// If we're still below file_end, loop. |
|||
cmp r4, r9 |
|||
blo 1b |
|||
// Clear out bss. |
|||
ldr r4, =_bss_start |
|||
ldr r9, =_bss_end |
|||
mov r5, #0 |
|||
mov r6, #0 |
|||
mov r7, #0 |
|||
mov r8, #0 |
|||
1: |
|||
// store multiple at r4. |
|||
stmia r4!, {r5-r8} |
|||
// If we're still below bss_end, loop. |
|||
cmp r4, r9 |
|||
blo 1b |
|||
// Call kernel_main |
|||
ldr r3, =kernel_main |
|||
blx r3 |
|||
// halt |
|||
halt: |
|||
wfe |
|||
b halt |
|||
</source> |
|||
New in this is the relocate chunk. The GPU loads the kernel.bin at address 0x8000 while it should be at _start (where that is is defined in the linker script). The relocate is a simple memcpy to put Raspbootin in the right place. Then the BSS is cleared and kernel_main is called like before. |
|||
====link-arm-eabi.ld==== |
|||
In the linker script change the start address: |
|||
<source lang=text> |
|||
ENTRY(Start) |
|||
SECTIONS |
|||
{ |
|||
/* Starts at LOADER_ADDR. */ |
|||
. = 0x2000000; |
|||
_start = .; |
|||
... |
|||
</source> |
|||
====main.cc==== |
|||
<source lang=cpp> |
|||
#include <stdint.h> |
|||
#include <uart.h> |
|||
extern "C" { |
|||
// kernel_main gets called from boot.S. Declaring it extern "C" avoid |
|||
// having to deal with the C++ name mangling. |
|||
void kernel_main(uint32_t r0, uint32_t r1, uint32_t atags); |
|||
} |
|||
#define LOADER_ADDR 0x2000000 |
|||
const char hello[] = "\r\nRaspbootin V1.0\r\n"; |
|||
const char halting[] = "\r\n*** system halting ***"; |
|||
typedef void (*entry_fn)(uint32_t r0, uint32_t r1, uint32_t atags); |
|||
// kernel main function, it all begins here |
|||
void kernel_main(uint32_t r0, uint32_t r1, uint32_t atags) { |
|||
UART::init(); |
|||
again: |
|||
UART::puts(hello); |
|||
// request kernel by sending 3 breaks |
|||
UART::puts("\x03\x03\x03"); |
|||
// get kernel size |
|||
uint32_t size = UART::getc(); |
|||
size |= UART::getc() << 8; |
|||
size |= UART::getc() << 16; |
|||
size |= UART::getc() << 24; |
|||
if (0x8000 + size > LOADER_ADDR) { |
|||
UART::puts("SE"); |
|||
goto again; |
|||
} else { |
|||
UART::puts("OK"); |
|||
} |
|||
// get kernel |
|||
uint8_t *kernel = (uint8_t*)0x8000; |
|||
while(size-- > 0) { |
|||
*kernel++ = UART::getc(); |
|||
} |
|||
// Kernel is loaded at 0x8000, call it via function pointer |
|||
UART::puts("booting..."); |
|||
entry_fn fn = (entry_fn)0x8000; |
|||
fn(r0, r1, atags); |
|||
// fn() should never return. But it might, so make sure we catch it. |
|||
// Wait a bit |
|||
for(volatile int i = 0; i < 10000000; ++i) { } |
|||
// Say goodbye and return to boot.S to halt. |
|||
UART::puts(halting); |
|||
} |
|||
</source> |
|||
====boot-server.cc==== |
|||
Don't put this in the kernel source directory. This is a standalone app and not part of the kernel. Compile and use with |
|||
<source lang=bash> |
|||
gcc -O2 -W -Wall -g -o boot-server boot-server.cc |
|||
./boot-server /dev/ttyUSB0 kernel/kernel.img |
|||
</source> |
|||
The boot-server handles uploading the kernel (second argument) to the RPi over the serial device (first argument). The boot-server is rather complex because it handles a few extra perks. You can unplug the USB serial adaptor (which is how I reboot my RPi) and replug it and it will reopen the device. Also the kernel is read from disk fresh every time the Raspbootin requests it. So you do not need to restart the boot-server every time you compile a new kernel. The boot-server switches between a simple console mode and uploading kernels when it detects the 3 breaks send by Raspbootin to initiate a kernel upload. So you can just leave it running all the time and use it as your RPi terminal as well. |
|||
<source lang=cpp> |
|||
#define _BSD_SOURCE /* See feature_test_macros(7) */ |
|||
#include <stdio.h> |
|||
#include <stdlib.h> |
|||
#include <sys/types.h> |
|||
#include <sys/stat.h> |
|||
#include <fcntl.h> |
|||
#include <unistd.h> |
|||
#include <string.h> |
|||
#include <errno.h> |
|||
#include <endian.h> |
|||
#include <stdint.h> |
|||
#include <termios.h> |
|||
#define BUF_SIZE 65536 |
|||
struct termios old_tio, new_tio; |
|||
void do_exit(int fd, int res) { |
|||
// close FD |
|||
if (fd != -1) close(fd); |
|||
// restore settings for STDIN_FILENO |
|||
if (isatty(STDIN_FILENO)) { |
|||
tcsetattr(STDIN_FILENO,TCSANOW,&old_tio); |
|||
} |
|||
exit(res); |
|||
} |
|||
// open serial connection |
|||
int open_serial(const char *dev) { |
|||
// The termios structure, to be configured for serial interface. |
|||
struct termios termios; |
|||
// Open the device, read/write, not the controlling tty, and non-blocking I/O |
|||
int fd = open(dev, O_RDWR | O_NOCTTY | O_NONBLOCK); |
|||
if (fd == -1) { |
|||
// failed to open |
|||
return -1; |
|||
} |
|||
// must be a tty |
|||
if (!isatty(fd)) { |
|||
fprintf(stderr, "%s is not a tty\n", dev); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
// Get the attributes. |
|||
if(tcgetattr(fd, &termios) == -1) |
|||
{ |
|||
perror("Failed to get attributes of device"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
// So, we poll. |
|||
termios.c_cc[VTIME] = 0; |
|||
termios.c_cc[VMIN] = 0; |
|||
// 8N1 mode, no input/output/line processing masks. |
|||
termios.c_iflag = 0; |
|||
termios.c_oflag = 0; |
|||
termios.c_cflag = CS8 | CREAD | CLOCAL; |
|||
termios.c_lflag = 0; |
|||
// Set the baud rate. |
|||
if((cfsetispeed(&termios, B115200) < 0) || |
|||
(cfsetospeed(&termios, B115200) < 0)) |
|||
{ |
|||
perror("Failed to set baud-rate"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
// Write the attributes. |
|||
if (tcsetattr(fd, TCSAFLUSH, &termios) == -1) { |
|||
perror("tcsetattr()"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
return fd; |
|||
} |
|||
// send kernel to rpi |
|||
void send_kernel(int fd, const char *file) { |
|||
int file_fd; |
|||
off_t off; |
|||
uint32_t size; |
|||
ssize_t pos; |
|||
char *p; |
|||
bool done = false; |
|||
// Set fd blocking |
|||
if (fcntl(fd, F_SETFL, 0) == -1) { |
|||
perror("fcntl()"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
// Open file |
|||
if ((file_fd = open(file, O_RDONLY)) == -1) { |
|||
perror(file); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
// Get kernel size |
|||
off = lseek(file_fd, 0L, SEEK_END); |
|||
if (off > 0x200000) { |
|||
fprintf(stderr, "kernel too big\n"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
size = htole32(off); |
|||
lseek(file_fd, 0L, SEEK_SET); |
|||
fprintf(stderr, "### sending kernel %s [%zu byte]\n", file, (size_t)off); |
|||
// send kernel size to RPi |
|||
p = (char*)&size; |
|||
pos = 0; |
|||
while(pos < 4) { |
|||
ssize_t len = write(fd, &p[pos], 4 - pos); |
|||
if (len == -1) { |
|||
perror("write()"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
pos += len; |
|||
} |
|||
// wait for OK |
|||
char ok_buf[2]; |
|||
p = ok_buf; |
|||
pos = 0; |
|||
while(pos < 2) { |
|||
ssize_t len = read(fd, &p[pos], 2 - pos); |
|||
if (len == -1) { |
|||
perror("read()"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
pos += len; |
|||
} |
|||
if (ok_buf[0] != 'O' || ok_buf[1] != 'K') { |
|||
fprintf(stderr, "error after sending size\n"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
while(!done) { |
|||
char buf[BUF_SIZE]; |
|||
ssize_t pos = 0; |
|||
ssize_t len = read(file_fd, buf, BUF_SIZE); |
|||
switch(len) { |
|||
case -1: |
|||
perror("read()"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
case 0: |
|||
done = true; |
|||
} |
|||
while(len > 0) { |
|||
ssize_t len2 = write(fd, &buf[pos], len); |
|||
if (len2 == -1) { |
|||
perror("write()"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
len -= len2; |
|||
pos += len2; |
|||
} |
|||
} |
|||
// Set fd non-blocking |
|||
if (fcntl(fd, F_SETFL, O_NONBLOCK) == -1) { |
|||
perror("fcntl()"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
fprintf(stderr, "### finished sending\n"); |
|||
return; |
|||
} |
|||
int main(int argc, char *argv[]) { |
|||
int fd, max_fd = STDIN_FILENO; |
|||
fd_set rfds, wfds, efds; |
|||
char buf[BUF_SIZE]; |
|||
size_t start = 0; |
|||
size_t end = 0; |
|||
bool done = false, leave = false; |
|||
int breaks = 0; |
|||
if (argc != 3) { |
|||
printf("USAGE: %s <dev> <file>\n", argv[0]); |
|||
printf("Example: %s /dev/ttyUSB0 kernel/kernel.img\n", argv[0]); |
|||
exit(EXIT_FAILURE); |
|||
} |
|||
// Set STDIN non-blocking and unbuffered |
|||
if (fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK) == -1) { |
|||
perror("fcntl()"); |
|||
exit(EXIT_FAILURE); |
|||
} |
|||
if (isatty(STDIN_FILENO)) { |
|||
// get the terminal settings for stdin |
|||
if (tcgetattr(STDIN_FILENO, &old_tio) == -1) { |
|||
perror("tcgetattr"); |
|||
exit(EXIT_FAILURE); |
|||
} |
|||
// we want to keep the old setting to restore them a the end |
|||
new_tio=old_tio; |
|||
// disable canonical mode (buffered i/o) and local echo |
|||
new_tio.c_lflag &= (~ICANON & ~ECHO); |
|||
// set the new settings immediately |
|||
if (tcsetattr(STDIN_FILENO, TCSANOW, &new_tio) == -1) { |
|||
perror("tcsetattr()"); |
|||
do_exit(-1, EXIT_FAILURE); |
|||
} |
|||
} |
|||
while(!leave) { |
|||
// Open device |
|||
if ((fd = open_serial(argv[1])) == -1) { |
|||
if (errno == ENOENT || errno == ENODEV) { |
|||
fprintf(stderr, "\r### Waiting for %s...\r", argv[1]); |
|||
sleep(1); |
|||
continue; |
|||
} |
|||
perror(argv[1]); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
fprintf(stderr, "### Listening on %s \n", argv[1]); |
|||
// select needs the largeds FD + 1 |
|||
if (fd > STDIN_FILENO) { |
|||
max_fd = fd + 1; |
|||
} else { |
|||
max_fd = STDIN_FILENO + 1; |
|||
} |
|||
done = false; |
|||
start = end = 0; |
|||
while(!done || start != end) { |
|||
// Watch stdin and dev for input. |
|||
FD_ZERO(&rfds); |
|||
if (!done && end < BUF_SIZE) FD_SET(STDIN_FILENO, &rfds); |
|||
FD_SET(fd, &rfds); |
|||
// Watch fd for output if needed. |
|||
FD_ZERO(&wfds); |
|||
if (start != end) FD_SET(fd, &wfds); |
|||
// Watch stdin and dev for error. |
|||
FD_ZERO(&efds); |
|||
FD_SET(STDIN_FILENO, &efds); |
|||
FD_SET(fd, &efds); |
|||
// Wait for something to happend |
|||
if (select(max_fd, &rfds, &wfds, &efds, NULL) == -1) { |
|||
perror("select()"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} else { |
|||
// check for errors |
|||
if (FD_ISSET(STDIN_FILENO, &efds)) { |
|||
fprintf(stderr, "error on STDIN\n"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
if (FD_ISSET(fd, &efds)) { |
|||
fprintf(stderr, "error on device\n"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
// RPi is ready to recieve more data, send more |
|||
if (FD_ISSET(fd, &wfds)) { |
|||
ssize_t len = write(fd, &buf[start], end - start); |
|||
if (len == -1) { |
|||
perror("write()"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
start += len; |
|||
if (start == end) start = end = 0; |
|||
// shift buffer contents |
|||
if (end == BUF_SIZE) { |
|||
memmove(buf, &buf[start], end - start); |
|||
end -= start; |
|||
start = 0; |
|||
} |
|||
} |
|||
// input from the user, copy to RPi |
|||
if (FD_ISSET(STDIN_FILENO, &rfds)) { |
|||
ssize_t len = read(STDIN_FILENO, &buf[end], BUF_SIZE - end); |
|||
switch(len) { |
|||
case -1: |
|||
perror("read()"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
case 0: |
|||
done = true; |
|||
leave = true; |
|||
} |
|||
end += len; |
|||
} |
|||
// output from the RPi, copy to STDOUT |
|||
if (FD_ISSET(fd, &rfds)) { |
|||
char buf2[BUF_SIZE]; |
|||
ssize_t len = read(fd, buf2, BUF_SIZE); |
|||
switch(len) { |
|||
case -1: |
|||
perror("read()"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
case 0: |
|||
done = true; |
|||
} |
|||
// scan output for tripple break (^C^C^C) |
|||
// send kernel on tripple break, otherwise output text |
|||
const char *p = buf2; |
|||
while(p < &buf2[len]) { |
|||
const char *q = index(p, '\x03'); |
|||
if (q == NULL) q = &buf2[len]; |
|||
if (p == q) { |
|||
++breaks; |
|||
++p; |
|||
if (breaks == 3) { |
|||
if (start != end) { |
|||
fprintf(stderr, "Discarding input after tripple break\n"); |
|||
start = end = 0; |
|||
} |
|||
send_kernel(fd, argv[2]); |
|||
breaks = 0; |
|||
} |
|||
} else { |
|||
while (breaks > 0) { |
|||
ssize_t len2 = write(STDOUT_FILENO, "\x03\x03\x03", breaks); |
|||
if (len2 == -1) { |
|||
perror("write()"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
breaks -= len2; |
|||
} |
|||
while(p < q) { |
|||
ssize_t len2 = write(STDOUT_FILENO, p, q - p); |
|||
if (len2 == -1) { |
|||
perror("write()"); |
|||
do_exit(fd, EXIT_FAILURE); |
|||
} |
|||
p += len2; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
close(fd); |
|||
} |
|||
do_exit(-1, EXIT_SUCCESS); |
|||
} |
|||
</source> |
|||
Enjoy. |
|||
==Parsing ATAGs== |
==Parsing ATAGs== |