树莓派玩转STM32开发(三)——先从点亮一个LED灯开始
在开始正式写代码之前,首先要解决一件事,那就是我们该如何去写这代码。
在树莓派搭建的Linux环境中,使用基于Xorg的桌面环境在本地开启图形化界面开发太耗费系统资源,这个方案首先不考虑。不过还剩下两种其他可行的方案,一是直接通过终端远程登陆进行开发,开发工作全部在文本模式下进行;另一种则使用Samba服务将Linux的文件系统挂载到Windows中,然后在Windows上面使用图形化界面的IDE开发。
如果你比较Geek,你可能会尝试远程登陆的方式,通过Vim或者Emacs两大神器之一进行开发。不过他们两者对新手不够友好,具体可以自己体验一下,不过网上有个段子:
问:如何生成一个随机字符串?
答:让新手退出vim.
而且在没有深入了解二者之前,容易被他们之间的“世纪大战”搞迷糊,然后进入一种着相1的状态:一定要对他们二者分一个高下,然后选择一个最好的使用。其实它们只是工具而已,自己用着爽就行了,不要自己把自己加入到程序员的鄙视链中,以开放的心态看待事物。笔者个人习惯使用Vim(别问,问就Vim),有丰富的插件,也可以自己对它扩展。或者试试VimPlus,一键帮你打造功能强大的Vim IDE(真香警告)。
VimPlus界面不过本系列是面向新同学的技术科普文章,在这里选用相对简单一点的方式,使用Samba服务将树莓派的文件系统通过网络挂载到Windows上,在Windows中使用VS Code进行开发。相比于Vim,VSCode使用起来简直不要太丝滑了。
首先在树莓派中安装Samba服务:
$ sudo apt install samba smbclient
安装samba
安装完成之后,需要配置它:
$ sudo cp /etc/samba/smb.conf /etc/samba/smb.conf.bak
$ sudo nano /etc/samba/smb.conf
在末尾添加,并保存:
[pi]
comment = user pi shared
path = /home/pi
available = yes
browseable = yes
public = yes
writable = yes
然后创建samba用户名和密码:
$ sudo touch /etc/samba/smbpasswd
$ sudo smbpasswd -a pi
然后会提示为这个用户创建一个新密码,可以随便指定,这里为了方便也设置为“pi”。完成之后要重启samba服务:
$ sudo systemctl restart smbd.service
此时Samba服务已经可用了,接下来在Windows中对其进行挂载:
此电脑->计算机->映射网络驱动器 输入samba服务地址 输入用户名和密码 网络驱动器映射成功请注意填写自己树莓派获取到的IP。如果出现异常,请看参考资料[1]尝试解决或者谷歌,还不行的话可以在文章下面评论或者私聊我~
接下来咋装VSCode我就不介绍了,把它装好之后我们就开始做点灯实验,首先在树莓派上把我准备的源码仓库拉下来,仓库中我会对每一个能跑的实例代码都会在相应的commit上面打上一个tag:
$ git clone https://github.com/Virus-V/Raspberry_STM32.git
$ cd Raspberry_STM32
$ git checkout led # 切换到 led tag 对应的commit
接下来执行编译并烧录到STM32中,烧录的时候要保证STM32开发板已经连接到树莓派上,同时lsusb
也能看到它:
~/Raspberry_STM32$ make -j
Build modules...
[...]
arm-none-eabi-objcopy -O binary led.elf led.bin
arm-none-eabi-objdump -h -S -D led.elf > led.lss
arm-none-eabi-nm -n led.elf > led.sym
Size after
led.elf :
section size addr
.isr_vector 268 134217728
.text 1172 134217996
.data 0 536870912
.bss 0 536870912
._user_heap_stack 512 536870912
.ARM.attributes 51 0
.debug_info 5639 0
.debug_abbrev 1581 0
.debug_aranges 560 0
.debug_ranges 496 0
.debug_line 2799 0
.debug_str 2652 0
.comment 43 0
.debug_frame 2324 0
Total 18097
Make Complete~
Sun Mar 15 02:34:41 UTC 2020
~/Raspberry_STM32$ sudo make download
Downloading led ...
Open On-Chip Debugger 0.10.0+dev-g4cfb2d54 (2020-03-12-12:36)
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
Info : clock speed 1000 kHz
Info : STLINK V2J24M11 (API v2) VID:PID 0483:374B
Info : Target voltage: 3.230159
Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : Listening on port 3333 for gdb connections
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000404 msp: 0x20005000
** Programming Started **
Info : device id = 0x20036410
Info : flash size = 128kbytes
** Programming Finished **
** Verify Started **
** Verified OK **
** Resetting Target **
shutdown command invoked
此时编译生成的代码已经下载到开发板中,不出意外的话,LED已经开始闪烁了。
在开发板上运行现在LED已经被成功点亮了,how it works?我们来扒一扒它内部的原理究竟是什么。
首先,我们编写的代码在Raspberry_STM32/User/main.c
中,它里面有三个函数:main, delay和Init_LED。main是我们程序逻辑的入口,里面首先调用Init_LED函数,初始化了GPIO外设。接下来就是一个大循环,里面重复执行着点亮——延时——熄灭——延时,如此往复。
int main() {
Init_LED();
while(1) {
GPIO_SetBits(GPIOC, GPIO_Pin_1);
delay();
GPIO_ResetBits(GPIOC, GPIO_Pin_1);
delay();
}
return 0;
}
现在看起来岁月静好,灯也按着我们的想法如期闪烁起来了。
但是,眼前的岁月静好,是因为有人在背后替我们负重前行。我们来思考几个问题:
- 代码下载到了哪里?芯片断电之后代码还在吗?
- 芯片是从main开始执行第一行代码的吗?如果不是,哪里才是它的第一行代码?
- 芯片怎么知道它要执行的第一行代码在哪里?代码是怎么执行起来的?
- 为什么我能通过代码控制LED灯?
回答这些问题之前,我们先看看STM32F103这个小东西肚子里都有什么。
STM32F103内部结构框图可以看到,STM32虽然黑不溜秋的一小坨趴在电路板上,它肚子里的东西还是很多的。
其实我们的代码下载到了片上Flash里,也就是上图标绿色的部分,这个东西是STM32片上的一块只读存储空间(ROM),断电之后不会丢失的。
STM32的第一行代码不是main函数,如果你仔细观察过编译过程,会发现有一个名为startup_stm32f10x_md.s
的文件被编译并链接到最终的可执行程序里,它才是芯片启动后第一行代码所在的那个文件。
.section .text.Reset_Handler
.weak Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
/* Copy the data segment initializers from flash to SRAM */
movs r1, #0
b LoopCopyDataInit
CopyDataInit:
ldr r3, =_sidata
ldr r3, [r3, r1]
str r3, [r0, r1]
adds r1, r1, #4
LoopCopyDataInit:
ldr r0, =_sdata
ldr r3, =_edata
adds r2, r0, r1
cmp r2, r3
bcc CopyDataInit
ldr r2, =_sbss
b LoopFillZerobss
/* Zero fill the bss segment. */
FillZerobss:
movs r3, #0
str r3, [r2], #4
LoopFillZerobss:
ldr r3, = _ebss
cmp r2, r3
bcc FillZerobss
/* Call the clock system intitialization function.*/
bl SystemInit
/* Call the application's entry point.*/
bl main
bx lr
STM32上电之后,首先从中断向量表中设置栈的起始位置,然后跳到中断向量表中的Reset_Handler
执行系统复位事件处理函数,它主要是初始化全局变量,将全局变量从ROM中搬移到RAM(上图标蓝色的部分),并且对BSS空间进行清零。BSS空间可以看作是默认值为0的全局变量所在的位置。全局变量初始化完成之后,调用SystemInit
对芯片进行初始化:打开外部高速时钟,初始化锁相环倍频至72MHz,初始化AHB总线时钟,APB1时钟和APB2时钟等初始化工作。最终才会调到我们编写的main函数中。
那么芯片怎么知道中断向量表在哪里呢?这个其实不难回答,中断向量表的地址是固化到芯片里的,而我们需要做的是把我们代码中的中断向量表放在那个固定的位置就OK了,放置的细节由链接器脚本文件来控制,这个文件是CMSIS/flash.ld
。芯片上电之后就会在那个位置取第一行代码去执行。
Cortex-M3是单发射顺序执行的标量处理器,内部是三级流水线,一个指令需要经过取指,译码,执行三个阶段才可以产生结果,然后退休(retire)。而指令执行过程中需要有寄存器配合才能做一些数据的处理。寄存器分为通用寄存器和专用寄存器。通用寄存器可以做任何用途,这些寄存器都由编译器分配如何使用,如果你手撕汇编的话最好遵循ATPCS或其他相应的调用规范;而专用寄存器是维护当前程序指令执行流状态的,包括栈顶指针SP,分支跳转返回地址LR,当前指令地址PC等。
正是因为这些处理器基础设施,才得以我们的代码有条不紊欢快地运行。
而最后一个问题,为什么我能通过代码控制LED灯呢?首先,LED连接到芯片IO口的引脚上,这个IO口属于GPIO,而GPIO作为一个外设(peripheral)挂到了APB2总线上。挂载到总线上的外设都有一系列寄存器,这些寄存器在整个32位的地址空间中都有一个唯一的地址,而我们的指令正是通过这些寄存器的地址来访问外设的这些配置寄存器,对其进行读写,从而达到了控制的目的。在GPIO中,一个IO口引脚对应其输出寄存器的一个bit,只要用指令将这个二进制位设置为0或者1,就可以控制相应的IO输出高电平或低电平。它内部实现的细节在此不深究,这属于数字电路的范畴,总之我们代码对外设所有的操作,都可以转化为对某个地址的读或写。
这也就让我想起了Unix设计哲学:一切皆文件,一切交互都可以抽象成读或者写。
参考资料
[1]. https://learnku.com/articles/36293
[2]. https://www.st.com/content/st_com/en/products/microcontrollers-microprocessors/stm32-32-bit-arm-cortex-mcus/stm32-mainstream-mcus/stm32f1-series/stm32f103/stm32f103rc.html
[1]. 着(zhuo)相是一个佛教术语,意思是执着于外相、虚相或个体意识而偏离了本质。“相”指某一事物在我们脑中形成的认识,或称概念。它可分为有形的(可见的)和无形的(也就是意识)。··