笔记 | Stanford CS49n 学习笔记:0 - blink 【LED闪烁】

本来是打算跟着 CS140e-win19的,但无奈资料不全,跟不下去,就弃坑了。感兴趣可以看看这个

笔记 | Stanford CS140e - 1:LED闪烁

[post cid="491" /]

​ 第一节课主要是熟悉树莓派的硬件,使用 bootloader 进行程序刷入等基本操作,但是作业要求的是实现点亮任意的pin(除保留以外),这一部分需要对基本的硬件知识有一些了解。

​ 关于内存映射的概念在计算机组成原理中基本了解,这里也算是一次复习,后面会详细整理思路。

​ 此次Lab的课程说明链接:Link

基本分为5个步骤:

  • 手动点亮LED(确保灯泡硬件正常)
  • 载入预编译程序 (检查内存卡及树莓派硬件是否正常)
  • 使用bootloader烧录程序(检查UART连接是否正常)
  • 编译给定汇编,烧录程序 (检查工具链 tool chain 环境)
  • 编写自己的程序并烧录

!> 一个额外的内容: 使用GPIO检测输入

从这个步骤来看,环环相扣,尽可能避免了错误的发生(或者说能够定位问题,不至于一头雾水不知道哪里错了)

这门课多了两个实现,第一个是两个LED的交替闪烁,会牵扯到对内存映射的准确理解

第二个是使用GPIO 作为输入端,我卡了好久,后面也记录了具体的思路。

!> 由于前面3部分的内容基本相同,因此只区分了不同的地方

1. 手动点亮 LED

​ 这里课程建议使用 CP2102 来为树莓派供电,(这是一个让桌面整洁的好点子,我买的带有短路保险,如果没有的话一定要注意手动接电阻,或者采用usb供电稳妥一些)。

​ 首先看一下树莓派的 GPIO 管脚图示:

!> 其中左上角的3.3v端口,在电路板上的焊接位置为方形

只需要将树莓派和 CP2102 连接对应的 5V 和接地的 GND 即可完成供电,可以观察到主办的电源灯点亮。

然后将 LED 与树莓派连接进行测试:

  • LED 的长脚为正极,与树莓派上的 3.3V 相连 (我试了试 5V, 没有烧坏嘻嘻嘻)
  • LED 的短脚为负极,连接树莓派上的 GND

此时可以观察到 LED 常亮,说明 LED 工作正常,可以进行下一步操作。

2. 载入预编译程序

按照要求操作即可(简单翻译下)

  • firmware 目录下的所有程序拷贝至格式化后的 SD 卡中
  • 将预编译程序 part/blink-pin20.bin 拷贝至 SD卡中并重命名为 kernel.img (替换原有的文件)
  • 安全弹出 SD 卡 (防止数据丢失)
  • 将 LED 连接到树莓派,正极接 GPIO20 (pin38)
  • 将 SD 卡插入树莓派并通电

可以观察到 LED 开始闪烁,频率大概是 2Hz

3. 使用bootloader烧录程序

i> 由于我使用的是 WSL, 因此不能检测到端口,(我的是 COM6 映射到 ttyS6), 而这节课直接给出了可执行文件没法改,所以我用了上节课的 rip-install.py 魔改了适用我的机器的 bootloader:rpi-install-wsl.py

如果每次烧板子都要取卡会疯的(有些板子内存还是嵌入的)

接下来可以使用 CP2102 传输数据,(它终于不是一个只会供电的小玩意了)

CP2102 的驱动安装网上资料一大堆,win10 还可以在设备管理器中单独设置下电源管理,不要自动关闭

简单说下操作:(LED的接线不需要改变,关闭电源取下内存卡即可)

firmware/bootloader.bin 文件拷贝至 SD 卡并命名为 kernel.img

连接 UART

  • TX: 数据发送
  • RX: 数据接收

所以交叉连一下就好

(和lab说明不太一样)将bin/rpi-install.py拷贝至 labs\lab1-blink,直接相对路径运行即可

lab1-blink文件夹打开 powershell 运行 python ./rpi-install.py part1/blink-pin20.bin

此时会将程序发送至树莓派,可以观察到传输成功后 LED 开始闪烁

可能需要补充的python库:sudo pip install {pyserial,xmodem,serial}, 用于检测计算机的usb设备。

4. 编译给定汇编,烧录程序 (检查工具链 tool chain 环境)【交叉编译】

这里需要将汇编语言编译为树莓派使用的二进制代码(ARM)

环境配置:

For a mac: (http://cs107e.github.io/guides/mac_toolchain/)

For ubuntu/linux (from: (https://github.com/eistec/mulle/wiki/Installing-toolchain-%28GCC%29)):

  sudo add-apt-repository ppa:team-gcc-arm-embedded/ppa
  sudo apt-get update
  sudo apt-get install gcc-arm-none-eabi

编译 part2/blink-pin20.s

  1. cd part2. 运行make.sh.
  2. 重新连接树莓派(必须断电(否则之前的程序一直在运行),且 CP2102 拔下来(否则会有端口占用的问题?)).
  3. lab1-blink文件夹打开 powershell 运行rpi-install.py part2/blink-pin20.bin

此时 LED 继续闪烁。

5. 编写自己的程序并烧录

这里需要用到一些硬件知识,参考 broadcom 文档: docs/BCM2835-ARM-Peripherals.pdf 90-96页.

NOTE: where the broadcom document uses addresses 0x7420xxxx, you'll use 0x2020xxxx.

给出的提示要求我们在赋值时采用位运算:

1
2
3
4
5
// assume: we want to set the bits 7,8,9 in <x> to <v> and
// leave everything else undisturbed.

x &=  ~(0b111 << 7); // clear the bits 7, 8, 9  in x
x |= (v << 7);     // or in the new bits

而不是简单的直接赋值,主要是考虑到多个 LED 同时控制的情况,可以自行尝试直接赋值控制多个LED。

这里做一点简单的解释:

GPFSELx: 这个32位的寄存器用于记录选择了哪几个pin口,由于每个方法码为3bit,每个寄存器控制10个pin

GPSETx: 这个32位寄存器记录对于pin口是否输出,每一位按顺序控制一个pin口

GPCLR0: 这个32位寄存器记录是否清空pin口的输出,每一位按顺序控制一个pin口

通过观察后面的表格可知,前面提到过方法码为3bit也在这里:

000 = GPIO Pin is an input,对应输入001 = GPIO Pin is an output,对应输出

其中[31:30]位保留,[29:0]位每三位控制一个gpio口,对于gpio有20多个的树莓派来说,使用GPFSEL0GPFSEL1GPFSEL2即可

即: GPFSEL0控制 GPIO9 - 0;GPFSEL1控制 GPIO19 - 10;GPFSEL2控制 GPIO29 - 20

这里顺序是反的,学过组成原理应该都懂,有点不清楚就多看看上面的pdf

到这里也就搞清楚了所有的对应关系,简单的求余计算 FSEL的编号,取模计算对应的3比特的位置即可。

下面放上代码,完成了任意端口的点亮,但是地址赋值这里也许可以改进,总之我想对指定地址的引用数据修改出现各种错误,就换了一种方式实现。

(设置评论再查看主要是鼓励自己动手

[hide]

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/*
 * write code to allow blinking using arbitrary pins.    
 * Implement:
 *	- gpio_set_output(pin) --- set GPIO <pin> as an output (vs input) pin.
 *	- gpio_set_on(pin) --- set the GPIO <pin> on.
 * 	- gpio_set_off(pin) --- set the GPIO <pin> off.
 * Use the minimal number of loads and stores to GPIO memory.  
 *
 * start.s defines a of helper functions (feel free to look at the assembly!  it's
 *  not tricky):
 *      uint32_t get32(volatile uint32_t *addr) 
 *              --- return the 32-bit value held at <addr>.
 *
 *      void put32(volatile uint32_t *addr, uint32_t v) 
 *              -- write the 32-bit quantity <v> to <addr>
 * 
 * Check-off:
 *  1. get a single LED to blink.
 *  2. attach an LED to pin 19 and another to pin 20 and blink in opposite order (i.e.,
 *     one should be on, while the other is off).   Note, if they behave weirdly, look
 *     carefully at the wording for GPIO set.
 */

// these are in start.s
void put32(volatile void *addr, unsigned v);
//void PUT32(int addr, unsigned v);
unsigned get32(const volatile void *addr);
//unsigned GET32(int addr);
void dumpy(int addr);

// see broadcomm documents for magic addresses.
#define GPIO_BASE 0x20200000
volatile unsigned *gpio_fsel0 = (volatile unsigned *)(GPIO_BASE);
volatile unsigned *gpio_fsel1 = (volatile unsigned *)(GPIO_BASE + 0x04);
volatile unsigned *gpio_fsel2 = (volatile unsigned *)(GPIO_BASE + 0x08);
volatile unsigned *gpio_set0  = (volatile unsigned *)(GPIO_BASE + 0x1C);
volatile unsigned *gpio_clr0  = (volatile unsigned *)(GPIO_BASE + 0x28); 

// set <pin> to output.  note: fsel0, fsel1, fsel2 are contiguous in memory,
// so you can use array calculations!
void gpio_set_output(unsigned pin) {
    // use gpio_fsel0
    int FSEL_offset = pin / 10;

    //*gpio_fsel1 = (0b001 << 18);
    volatile unsigned *gpio_fsel; 
    switch (FSEL_offset) {
        case 0:
            gpio_fsel = gpio_fsel0;
            break;
        case 1:
            gpio_fsel = gpio_fsel1;
            break;
        case 2:
            gpio_fsel = gpio_fsel2;
            break;
        default:
            gpio_fsel = gpio_fsel0;
    }
    *gpio_fsel &= ~(0b111 << ((pin % 10) * 3)); 
    *gpio_fsel |= 0b001 << ((pin % 10) * 3);  // set the pin outset <pin> on.
    
}

void gpio_set_on(unsigned pin) {
    // use gpio_set0
	*gpio_set0 = 1 << pin;
}

// set <pin> off
void gpio_set_off(unsigned pin) {
    // use gpio_clr0
	*gpio_clr0 = 1 << pin;
}

// For later labs, write these routines as well.

// set <pin> to input.
void gpio_set_input(unsigned pin) {
    // use gpio_fsel0 
    int FSEL_offset = pin / 10;
    volatile unsigned *gpio_fsel;
    switch (FSEL_offset) {
        case 0:
            gpio_fsel = gpio_fsel0;
            break;
        case 1:
            gpio_fsel = gpio_fsel1;
            break;
        case 2:
            gpio_fsel = gpio_fsel2;
            break;
        default:
            gpio_fsel = gpio_fsel0;
    }
    *gpio_fsel &= ~(0b111 << ((pin % 10) * 3)); 
    *gpio_fsel |= 0b000 << ((pin % 10) * 3);  // set the pin outt <pin> to <v> (v \in {0,1})
}

void gpio_write(unsigned pin, unsigned v) {
    // 
	return;
}

// countdown 'ticks' cycles; the asm probably isn't necessary.
void delay(unsigned ticks) {
    while(ticks-- > 0)
        asm("add r1, r1, #0");
}

/*
void reboot(void) {
    const int PM_RSTC = 0x2010001c;
    const int PM_WDOG = 0x20100024;
    const int PM_PASSWORD = 0x5a000000;
    const int PM_RSTC_WRCFG_FULL_RESET = 0x00000020;
    int i;
    for(i = 0; i < 100000; i++)
         dummy(i);
    PUT32(PM_WDOG, PM_PASSWORD | 1);
    PUT32(PM_RSTC, PM_PASSWORD | PM_RSTC_WRCFG_FULL_RESET);
    while(1);
}
*/

// when you run should blink 10 times. will have to restart the pi by pulling the usb connection out.
void notmain(void) {
    int led = 16;
    int led2 = 20;
    gpio_set_output(led);
    gpio_set_output(led2);
    for(int i = 0; i < 10; i++) {
        gpio_set_on(led);
        gpio_set_off(led2);
        delay(1000000);
        gpio_set_on(led2);
        gpio_set_off(led);
        delay(1000000);
    }
//	reboot();
}

[/hide]

6. 额外内容

1
2
3
4
5
int main(void) {
	int v = 5;
	int *p = &v;
    return 0;
}
        .file   "foo.c" 
        .text
        .section        .text.startup,"ax",@progbits
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        xorl    %eax, %eax
        ret
        .cfi_endproc
.LFE0: 
        .size   main, .-main
        .ident  "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04) 7.4.0"
        .section        .note.GNU-stack,"",@progbits
1
2
3
4
5
int main(void) {
	volatile int v = 5;
	volatile int *p = &v;
    return 0;
}
        .file   "foo.c" 
        .text
        .section        .text.startup,"ax",@progbits
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        movl    $5, -4(%rsp) 
        xorl    %eax, %eax
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04) 7.4.0"
        .section        .note.GNU-stack,"",@progbits

可以注意到不加 volitale 时编译器省略了赋值的步骤,但简单地加上输出,就不会省略(优化)赋值步骤,如下

1
2
3
4
5
6
7
#include<stdio.h>
int main(void) {
	int v = 5;
	int *p = &v;
	printf("%d", v);
    return 0;
}
        .file   "foo.c"
        .text
        .section        .rodata.str1.1,"aMS",@progbits,1
.LC0:
        .string "%d"
        .section        .text.startup,"ax",@progbits
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB23:
        .cfi_startproc
        leaq    .LC0(%rip), %rsi
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $5, %edx
        movl    $1, %edi
        xorl    %eax, %eax
        call    __printf_chk@PLT
        xorl    %eax, %eax
        addq    $8, %rsp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE23:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04) 7.4.0"
        .section        .note.GNU-stack,"",@progbits

添加重启代码部分不是很清楚,大概后面还会讲就先跳过了。。。如果有完成了所有extra部分的欢迎交流。

7. GPIO检测输入

最后在part4文件夹下,有关于使用GPIO作为输入端的lab说明Link

观察图示可知,对于检测输入,首先需要设置 Pull Up Down

Up: 3.3v

Down: 内部有一个很大的电阻,类似断路?

其次,我采用了Level Detect,检测电平状态。

Edge Detect 我理解的是就是时钟沿信号的翻转敏感

因此思路就是,将端口设置为Pull Down, 检测高电平High Level Detect来打开Led。

不过Level Detect可以认为是使能信号,最终的检测还需要在 Event Detect中查看。

关于信号的设置,相信看了前面的内容应该都知道怎么用了,就不再赘述,再放下pdf

!> 可能是我理解不对,这么做下去灯点亮之后不会灭,因此我又交替进行高低电平的检测,在低电平时关闭LED, 如果有更好的方案欢迎私戳

最后放下代码,默认隐藏:

[hide]

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/*
 * write code to allow blinking using arbitrary pins.    
 * Implement:
 *	- gpio_set_output(pin) --- set GPIO <pin> as an output (vs input) pin.
 *	- gpio_set_on(pin) --- set the GPIO <pin> on.
 * 	- gpio_set_off(pin) --- set the GPIO <pin> off.
 * Use the minimal number of loads and stores to GPIO memory.  
 *
 * start.s defines a of helper functions (feel free to look at the assembly!  it's
 *  not tricky):
 *      uint32_t get32(volatile uint32_t *addr) 
 *              --- return the 32-bit value held at <addr>.
 *
 *      void put32(volatile uint32_t *addr, uint32_t v) 
 *              -- write the 32-bit quantity <v> to <addr>
 * 
 * Check-off:
 *  1. get a single LED to blink.
 *  2. attach an LED to pin 19 and another to pin 20 and blink in opposite order (i.e.,
 *     one should be on, while the other is off).   Note, if they behave weirdly, look
 *     carefully at the wording for GPIO set.
 */

// these are in start.s
void put32(volatile void *addr, unsigned v);
//void PUT32(int addr, unsigned v);
unsigned get32(const volatile void *addr);
//unsigned GET32(int addr);
void dumpy(int addr);

// see broadcomm documents for magic addresses.
#define GPIO_BASE 0x20200000
volatile unsigned *gpio_fsel0 = (volatile unsigned *)(GPIO_BASE);
volatile unsigned *gpio_fsel1 = (volatile unsigned *)(GPIO_BASE + 0x04);
volatile unsigned *gpio_fsel2 = (volatile unsigned *)(GPIO_BASE + 0x08);
volatile unsigned *gpio_set0  = (volatile unsigned *)(GPIO_BASE + 0x1C);
volatile unsigned *gpio_clr0  = (volatile unsigned *)(GPIO_BASE + 0x28); 
volatile unsigned *gpio_lev0 = (volatile unsigned *)(GPIO_BASE + 0x34);
// set <pin> to output.  note: fsel0, fsel1, fsel2 are contiguous in memory,
// so you can use array calculations!
void gpio_set_output(unsigned pin) {
    // use gpio_fsel0
    int FSEL_offset = pin / 10;

    //*gpio_fsel1 = (0b001 << 18);
    volatile unsigned *gpio_fsel; 
    switch (FSEL_offset) {
        case 0:
            gpio_fsel = gpio_fsel0;
            break;
        case 1:
            gpio_fsel = gpio_fsel1;
            break;
        case 2:
            gpio_fsel = gpio_fsel2;
            break;
        default:
            gpio_fsel = gpio_fsel0;
    }
    *gpio_fsel &= ~(0b111 << ((pin % 10) * 3)); 
    *gpio_fsel |= 0b001 << ((pin % 10) * 3);  // set the pin outset <pin> on.
    
}

void gpio_set_on(unsigned pin) {
    // use gpio_set0
	*gpio_set0 = 1 << pin;
}

// set <pin> off
void gpio_set_off(unsigned pin) {
    // use gpio_clr0
	*gpio_clr0 = 1 << pin;
}

// For later labs, write these routines as well.

// set <pin> to input.
void gpio_set_input(unsigned pin) {
    // use gpio_fsel0 
    int FSEL_offset = pin / 10;
    volatile unsigned *gpio_fsel;
    switch (FSEL_offset) {
        case 0:
            gpio_fsel = gpio_fsel0;
            break;
        case 1:
            gpio_fsel = gpio_fsel1;
            break;
        case 2:
            gpio_fsel = gpio_fsel2;
            break;
        default:
            gpio_fsel = gpio_fsel0;
    }
    *gpio_fsel &= ~(0b111 << ((pin % 10) * 3)); 
    *gpio_fsel |= 0b000 << ((pin % 10) * 3);  // set the pin outt <pin> to <v> (v \in {0,1})
}

void gpio_write(unsigned pin, unsigned v) {
    *gpio_lev0 |= (v << pin); 
	return;
}

// countdown 'ticks' cycles; the asm probably isn't necessary.
void delay(unsigned ticks) {
    while(ticks-- > 0)
        asm("add r1, r1, #0");
}

// when you run should blink 10 times. will have to restart the pi by pulling the usb connection out.
void notmain(void) {
    int led_in = 20;
    int led_out = 21;
    gpio_set_output(led_out);
    gpio_set_input(led_in);
    volatile unsigned tmp = 1 << led_in;

    // event detected
    volatile unsigned *gpio_eds0 = (volatile unsigned *)(GPIO_BASE + 0x40); // gpio event detect status register0
    *gpio_eds0 |= (1 << led_in);
    // high detect enable 
    volatile unsigned *gpio_hen0 = (volatile unsigned *)(GPIO_BASE + 0x64);
    *gpio_hen0 = 1 << led_in;
    // low detect enable 
    volatile unsigned *gpio_len0 = (volatile unsigned *)(GPIO_BASE + 0x70);
    //gio_set_output(12);
    while(1) {
        *gpio_eds0 = tmp;
        // high level detected
        if (*gpio_eds0 == tmp && *gpio_hen0 == tmp) {
            *gpio_hen0 = 0 << led_in;   // disable high detected
            *gpio_len0 = 1 << led_in;   // enable low detected
            gpio_set_on(led_out);       // turn on led
        } else
        // low level detected
        if (*gpio_eds0 == tmp && *gpio_len0 == tmp) {
            *gpio_len0 = 0 << led_in;   // disable low detected
            *gpio_hen0 = 1 << led_in;   // enable hign detected
            gpio_set_off(led_out);
        }
        *gpio_eds0 = 0;
        delay(1000000);
    }
}

[/hide]

这里我设置了pin20为输出端, pin21为输入端,结果就是,当连接3.3vpin21时,LED 会点亮

x> 最后要提示的是,这里 pin21 设置为 Pull down,因此本身有电阻保护,但是安全起见,建议结合Link中的说明使用面包板和电阻。

i> 我使用的cp2102自带短路保护,所以不担心,直接连的(甚至用短路来辅助重启。。。划掉)

i> 建议手头材料不足的朋友可以将led作为电阻使用(反正能够满足电平检测),但我不小心烧坏了一只,好像是不小心接反了。【电路就不画了,中学物理】

updatedupdated2023-01-302023-01-30
点击刷新