阅读视图

发现新文章,点击刷新页面。
🔲 ⭐

浅聊 esp8266

PCB

最近在做 E-ink Todo List 项目,用到了 ESP8266,所以就简单的聊一下 ESP8266。

E-ink Todo List 网站

ESP8266EX

ESP8266 是由乐鑫科技推出的一个芯片系列,ESP8266EX 是其中的一款。

ESP8266EX Pin Layout

ESP8266EX 提供了完整 TCP/IP 协议栈和 WiFi 功能,一个 10 位 ADC,支持 SPI、I2C、UART、PWM 等接口。是一个非常适合用于做一些需要无线联网且由电池供电的项目的芯片。

ESP-12F 模组

ESP-12F 模组

为了快速创造我想要的东西,我使用了安可信的 ESP-12F 模组。使用 SMD 封装方式。它集成了 ESP8266EX 芯片,WiFi 天线和外部闪存的模组。

SMD 是 Surface-Mounted Device(表面贴装设备)的缩写。起源于 1960 年代,最初由美国 IBM 公司进行技术研发,之后于 1980 年代后期渐趋成熟。它的特点是元件直接贴装在电路板的表面,而不需要通过电路板的孔进行焊接。这种封装方式使得元件可以更小,更轻,而且可以在电路板的两面进行贴装。SMD0805 和 SMD0603 是常见的 SMD 封装规格,0805 封装的尺寸是 0.08 英寸 x 0.05 英寸( 2.0mm x 1.25mm),0603 封装的尺寸是 0.06 英寸 x 0.03 英寸(1.6mm x 0.8mm)。

ESP-12F 是特殊尺寸的 SMD 封装。

ESP-12F Pin Layout

固件下载

项目中用了 USB 转 TTL 串口芯片 CH340C 通过下面的引脚来实现自动下载固件和进入运行模式。

  • ENABLE

    使能引脚,当使能引脚为高电平时,芯片或模块处于工作状态;当使能引脚为低电平时,芯片或模块处于待机或关断状态。

  • GPIO0(FLASH)

    用于控制 ESP8266EX 进入下载模式。当 GPIO0 为低电平时, 让 ENABLE 引脚上升沿(从低电平到高电平)时,ESP8266EX 将进入下载模式。当 ESP8266EX 处于运行模式时,GPIO0 引脚可作为普通的输入输出引脚使用。

  • GPIO1(TX)

    UART 通用串口通讯的接收引脚

  • GPIO3(RX)

    UART 通用串口通讯的接收引脚

我会单独写一篇文章详细介绍自动下载电路的原理。

SPI 电路

SPI(Serial Peripheral Interface) 是一种芯片与芯片的外围设备通信协议。需要用到 4 条线路,分别是:

  • SCK(Serial Clock)

    串行时钟信号,由主设备产生,用于对齐数据传输的间隔。

  • MOSI(Master Out Slave In)

    主设备输出,从设备输入,主设备发送数据到从设备。

  • MISO(Master In Slave Out)

    主设备输入,从设备输出,从设备发送数据到主设备。

  • SS(Slave Select) 或 CS(Chip Select)

    从设备选择信号,由主设备产生,用于选择从设备。

我在项目中使用 SPI 通信方式与 E-ink 屏幕通信。除了上面的 4 条线路外,还用要控制 E-ink 屏幕的复位,以及读取 E-ink 屏幕是否忙碌的状态。

我在 ESP-12F 上使用了下面的引脚:

ESP-12FE-ink 驱动电路功能
GPIO2RST控制屏幕复位的引脚,低电平复位屏幕
GPIO4DC控制引脚屏幕的数据和命令模式,高电平表示数据,低电平表示命令
GPIO5BUSY屏幕忙碌状态输出引脚,高电平表示屏幕忙碌,低电平表示屏幕空闲
GPIO13(MOSI)DINSPI 通信 MOSI 引脚
GPIO14(SCK)CLKSPI 通信 SCK 引脚
GPIO15(CS)CSSPI 片选引脚

ADC 引脚

ESP-12F 有一个 10 位 ADC,可以用来读取模拟量信号。我将会用这个引脚来读取电池电压。ADC0 引脚可读取 0-1V 的电压。需要使用一个电阻分压电路将电池电压分压到 0-1V 范围内再通过 ADC0 引脚读取。

编程平台

ESP8266EX 的开发环境有很多,我选择了 PlatformIO。一开始我是使用 Arduino IDE 来开发的,但是 Arduino IDE 的功能太过简单,不适合太复杂的项目。

我使用 VSCode 安装了 PlatformIO 插件,通过 PlatformIO 的 CLI 工具来编译和下载固件。

参考

广告

本文由我在制作 E-ink Todo List 墨水屏待办 DIY 的 PCB 板时总结的,我还写了一篇《墨水屏 Todo List 制作教程》

👋 E-ink Todo List 墨水屏待办 DIY 的 PCB 现已上架售卖 😇,此 PCB 板可代替教程中的开发板和屏幕转接板。爱好 DIY 的朋友们可以下单购买制作。

E-ink Todo List 墨水屏待办的成品正在起来的路上!!!

下面是 PCB 板的购买途径。

EsonWong 的微信小店

E-ink Todo List PCB
🔲 ☆

浅谈 RISC-V 软件开发生态之 IDE

0x00 前言

今天简单谈一些关于 RISC-V 开发的软件生态相关,主要是关于 RISC-V 的开发 IDE,就是集成开发环境。集成开发环境(IDE,Integrated Development Environment )是用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具。集成了代码编写功能、分析功能、编译功能、调试功能等一体化的软件开发服务套(组)。

目前来看,RISC-V 的硬件生态已经在蓬勃发展,而 RISC-V 相关的软件生态还在日趋完善的过程中,这里就来浅谈一点我的个人认识,如有不对,请批评指正。

目前,RISC-V 的软件开发 IDE ,主要是有以下几种解决方案。

0x01 eclipse —— 著名开源 IDE

全开源,需要自行集成 RISC-V Toolchains + eclipse + OpenOCD 来搭建开发环境。

优点:

  1. 全开源,自由,免费 的 IDE
  2. 有众多公司厂商,组织,支持维护
  3. 可扩展的插件功能

缺点:

  1. 庞大臃肿,而且需要 java 运行环境。现在的版本在安装过程中会自动安装一个 jre 的运行环境。但是目前国内用户如果自行安装会下载很慢,使用代理相对快很多。如果不使用安装版本,也可以使用 zip 包解压的版本
  2. 集成众多插件,插件数量多了之后会拖慢系统
  3. 需要自行集成编译器及仿真器,打包发布给用户使用

0x02 IAR for RISC-V—— 老牌商业 IDE

IAR for RISC-V 版本目前已经正在和部分芯片厂商进行合作适配、授权支持。主要包括:SiFive、Andes Technology、Nuclei(芯来科技)、CloudBEAR、Syntacore、C-SKY(平头哥)、GigaDevice(兆易创新)、MicroChip。

优点:

  1. IAR 有非常优秀的编译器,针对代码的大小和速度有很好的优化;从 coremark 跑分排行榜上的 ARM 芯片来看,相对来说 IAR 的代码性能更高一些,当然各版本之间也会存在差异。但目前 IAR for RISC-V 的版本还未开放下载,暂时还没有相关测评。
  2. 有很优秀的 Trace 工具进行调试

coremark

IAR for RISC-V

缺点:

  1. 纯商业 IDE,使用需要授权,相对来说用户不易获得授权使用,尤其是新版本更新了 License 授权方案
  2. RISC-V 芯片需要通过其 i-jet 来调试

0x03 Embedded Studio for RISC-V —— SEGGER 老牌嵌入式开发工具供应商

Embedded Studio 目前正在适配支持 Andes Technology、Nuclei(芯来科技)、GigaDevice(兆易创新) 、SiFive、Syntacore、Western Digital 的 RISC-V 芯片开发。

优点:

  1. 个人用户免费,支持全功能, 跨平台支持含 Windows, macOS 和 Linux 版本。
  2. 优于 IAR 的编辑器
  3. 启动速度快,大工程启动速度相对较快
  4. 调试工具丰富,因为是传统嵌入式工具链厂商,有 J-link 系列工具支持
  5. 支持 RISC-V 内核

缺点:

  1. 目前只支持 J-link 进行 debug
  2. 设置选项较为烦锁

0x04 VS code —— 开发新秀

VS code 在我看来就是一款优秀的开源跨平台代码编辑器,但由于其内置了标准 Debugger Adaptor Protocol,经过各路大神,一些组织,部分企业公司结合各自的需求,开发了各具特色的 Debug 插件。于是乎 VS Code 俨然变成了一款极具竞争力的 IDE,成功跨界抢各类 IDE 的市场,因为其灵活小巧,迅速成为了各路开发者的新宠。但也由于其开发调试功能不是本身内置,也给不同的开发需求带来了一定的门槛,需要开发者自行进行一些开发环境配置,对于新手小白不是特别友好。但还是有很多愿意折腾的大佬。

优点:

  1. 软件全开源免费,体积小,启动快,界面新颖,更新快,新兴的优秀编辑器代表
  2. 开源众多的插件
  3. 可跨平台使用

缺点:

  1. 其本身就是一个,优秀的开源编辑器,如果要进行 MCU 开发,需要开发插件,或者使用相应的工具链来自行配置,对用户不是很友好
  2. 目前没有支持 RISC-V 的通用插件
  3. 通过体验 cortex debug 、esp-idf、platformIO 等开发插件,体验也并不是很友好;但也是跟插件开发者的能力、需求和习惯相关
  4. Cortex Debug 插件,代码在 bootrom 里运行时,反汇编窗口无法显示当前的 bootrom 代码,除非手动反汇编。它执行显示当前 elf 范围内的文件,regs 窗口没法设置显示格式;这些可能是 Cortex Debug 插件的局限。

0x05 KendryteIDE —— 基于 VS code 包装的 IDE 方案

KendryteIDE 是嘉楠勘智,基于开源的 VS code 编辑器,自己定制的 RISC-V 芯片 IDE 解决方案,整体风格继承 VS code。

优点:

  1. 基于开源 VS code,二次开发,完全自主可控,轻量级
  2. 继承了 VS code 的优秀编辑器,及其优点

缺点:

  1. 目前不可以直接使用其 IDE,来进行其他的 RISC-V 芯片调试
  2. 需要重新适配自己的 MCU 来做开发,开发工作量和时间周期是不确定的
  3. 定制程度取决于开发人员的能力

0x05 总结

可能正是由于 RISC-V 硬件的自由更改的属性,各家厂商都可以有自己独特的 RISC-V 架构,所以这也导致了 RISC-V 的编译工具链会有各厂商自己定制,不能像 ARM 那样各个厂商都使用 MDK,只需要做一个 SDK or Pack 包集成到 MDK 中即可。于是乎,我们就看到了市面上的各大 RISC-V 芯片或 IP 公司,都在做自己的 IDE 用自己的工具链。
那么为什么各家都在做各自的工具链呢,我认为还是没有形成类似于 ARM 的 CMSIS 这样的嵌入式软件接口标准,来统一管理底层软件接口,于是乎就变成了各自玩各自的,没有统一。这也是导致 RISC-V 软件生态薄弱,碎片化的一个重要因素。

但我相信,未来应该也会出现类似于 CMSIS 的标准,来完成一统大业的工作

于是乎现在就是八仙过海,各显神通的局面。但大部分的 RISC-V 厂商的开发 IDE,还是基于开源的 eclipse + gcc toolchains + openocd 的方案来开发和调试芯片产品,相对来说这可能是比较快和相对成熟的方案。

当然,我个人猜测,像 IAR、SEGGER 这种纯商业的第三方 IDE 、嵌入式工具供应商,也希望能够适配市面上的各型号 MCU 开发,稳固自己的工具链生态,所以 SEGGER 率先推出了支持 RISC-V 开发的 IDE,但可能也正是由于 RISC-V 的灵活性,致使全面支持 RISC-V 架构的 IAR 版本还迟迟没有正式推出。

个人认为,如果一个 RISC-V 芯片厂商需要尽快推出自己的 IDE ,那么可能使用 eclipse + gcc toolchains + openocd 的方案会比较快;如果时间不急的话,在目前商业 IDE 还不成熟的情况下,可能自行研发是比较好的选择,对其自己的芯片开发的适配程度也会更高;那么自行研发也有两个方向,一个就是基于 VS code 编辑器做二次开发,参考嘉楠勘智方案;另一个就是从头开发一款自己的 IDE,但由于自己定制调试器也是一个时间周期比较长的过程,所以大概率底层还是 gcc + openocd。当然了,这里也还有未列出的阿里平头哥发布的 剑池 CDK 开发环境,他们有中天微时期的基础,所以他们就是自己完全定制的 IDE,并且有自己的调试器 ck-link,是解决方案比较成熟的厂商了。

eclipse VS code IAR Embedded Studio
版权 开源,免费 开源,免费 商业授权 个人用户免费,合作厂商用户免费
是否支持 RISC-V 编辑器本身不支持,可定制 编辑器本身不支持,可定制 支持,但需要厂商和 IAR 合作开发 支持,需要厂商合作开发
是否插件拓展 有插件 有插件,没有 RISC-V 通用插件,需要厂商 or 第三方 or 开源社区自定义 不支持 不支持
是否可调试 使用开源 openocd 调试 使用开源 openocd 调试 支持 RISC-V 的版本需要使用 IAR 官方 I-jet 仿真器 支持 RISC-V,但仅支持 SEGGER 的 J-link 仿真器
快捷键调试 支持 各种调试插件使用方式不一致 支持 支持
汇编 Debug 支持 需要自定义插件支持 支持 支持
Mem 访问、读写 支持 需要自定义插件支持 支持 支持
寄存器访问、读写 支持 需要自定义插件支持 支持 支持
窗口中变量、数据的格式是否可更改 支持 不确定,可能需要自定义插件支持 支持 支持
用户界面 新版本有所改进,有颜色主题更改 新潮,有较多颜色主题,代码配色友好 新版本有有限主题更改 有限更改
…… …… …… …… ……
🔲 ☆

简单了解一下小米 vela

0x00 小米 vela

前段时间小米推出了 vela 物联网平台,vela 就是基于 NuttX 打造的物联网开发平台。

小米对 NuttX 的评价:

  1. NuttX 对 POSIX 标准有原生兼容:NuttX 是可商用化 RTOS 中唯一一个对 POSIX API 有原生支持的实时操作系统,所以很多 Linux 社区的开源软件可以很方便的移植到 NuttX 上,这样可以极大的简化开源软件移植,方便代码复用,降低学习曲线,其它 RTOS 需要适配层把 POSIX API 转成内部 API,而且通常只兼容一小部分的 POSIX 接口。
  2. 完成度高:NuttX 集成了文件系统、网络协议栈、图形库和驱动框架,减少开发成本。
  3. 模块化设计:所有组件甚至组件内部特性,都可以通过配置 Kconfig 来调整或关闭,可按需对系统进行裁剪,适用于不同产品形态。
  4. 代码精简:所有组件都是从头编码,专门对代码和数据做了优化设计。
  5. 轻量级:虽然 NuttX 实现了传统操作系统的所有功能,但是最终生成的代码尺寸还是可以很小(最小配置不到 32KB,最大配置不超过 256KB)。
  6. 和 Linux 系统的兼容性:因为 NuttX 整体设计、代码组织,编译过程和 Linux 非常接近,将会极大地降低 Android/Linux 开发者的迁移成本。
  7. 活跃开放的社区:很多厂商(比如小米、Sony,乐鑫、NXP 等)和开源爱好者都在积极回馈社区。

不难看出小米对 NuttX 的评价很不错。所以我也来赶紧学习一波,做了一个简单的了解,作为自己的技术储备。

0x01 安装配置

环境: Ubuntu20.04 系统、ARM GUN 2019q4 版本 gcc、openocd 0.10 版本

安装 ARM gcc 工具链,如果开发板是用其他的架构 cpu 请去安装其他架构下相应的版本工具链,我这里是 ARM 的。

1
2
3
4
5
sudo mkdir /opt/gcc
sudo chgrp -R users /opt/gcc
cd /opt/gcc
wget https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2019q4/gcc-arm-none-eabi-9-2019-q4-major-x86_64-linux.tar.bz2
tar xf gcc-arm-none-eabi-9-2019-q4-major-x86_64-linux.tar.bz2

配置工具链到系统环境变量

1
echo "export PATH=/opt/gcc/gcc-arm-none-eabi-9-2019-q4-major/bin:$PATH" >> ~/.bashrc

下载 nuttx 、apps、tools 三个 nuttx 系统源码及构建工具

1
2
3
4
5
mkdir nuttxWS
cd nuttxWS
git clone https://github.com/apache/incubator-nuttx.git nuttx
git clone https://github.com/apache/incubator-nuttx-apps apps
git clone https://bitbucket.org/nuttx/tools.git tools

安装 kconfig ,构建工具。注意:低于 ubuntu20 版本的安装要少许麻烦一些

1
$ apt install kconfig-frontends

查看 nuttx 支持的板卡

1
2
$ cd nuttxWS
$ ./tools/configure.sh -L | less

选择相应的板卡及其支持的应用程序进行配置

1
2
3
4
$ cd nuttxWS
$ ./tools/configure.sh -l <board-name>:<config-dir>
# for instance:
$ ./tools/configure.sh -l stm32f103-minimum:nsh

运行 menuconfig 进行自定义配置

1
$ make menuconfig

0x02 编译运行

配置完成后编译 NuttX 系统

1
$ make -j$(nproc)

编译完成后会在 nuttx 目录下生成一个 nuttx.bin 文件,接下来把他下载到板子是运行
首先安装 openocd

1
$ sudo apt install openocd

如果使用 apt 安装的 openocd 版本过低,就自己从源码安装一下 openocd 即可

Openocd 安装好之后,正确连接板卡和电脑,下载程序。
烧写程序之前可以先打开串口,当系统正常运行起来之后可以在串口中观察到 nsh> 命令行。注意,我这边是 stlink 的虚拟串口所以是 ACM0,你的如果不是虚拟串口,可能是 USB0 之类的。

1
$  picocom -b 115200 /dev/ttyACM0

下载程序需要注意正确选择你使用的下载器和板卡对应的 .cfg 文件

1
2
$ cd nuttxWS
$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f1x.cfg -c 'init' -c 'program nuttx.bin 0x08000000 verify reset' -c 'shutdown'

如果程序正常运行了就可以在中观察到信息,输入 help 命令就可以查看支持的命令列表。如果没有看到信息,reset 一下板子。

修改配置编译一个 blinking 控制 led 的程序

回到 nuttx 目录下清除原来的配置,重新生成,在 menuconfig 中检查 led 相关配置是否设置成功。在 NSH library 中使能 printf 功能。

1
2
3
4
5
$ cd nuttxWS/nuttx
$ make distclean
$ ./tools/configure.sh -l stm32f103-minimum:userled
$ make menuconfig
$ make -j$(nproc)

编译完成后下载到板卡上,打开串口,按一下 reset 连接上板子就可以在串口看到输出的信息了。直接执行 leds ,可以看到板卡上的 led 已经正常运行起来了。

今天就先点个灯了解一下,其他的内容,等我翻翻 NuttX 源码在学习一下。

🔲 ☆

关于 RISC-V 架构下 RTOS 的一些知识

0x00

之前的 blog 有介绍了一些,wujian100 的一些知识,包括综合、测试等。最近就想在 wujian100 上看看能不能移植一下比较常见的一些 RTOS (Real Time Operating System,实时操作系统)上去试试,比如 Free RTOS、RT-Thread等。结果发现这里还是有一些坑的。虽然 FreeRTOS 和 RTT 都支持 RISC-V 的芯片了,但是 wujian100 这个是 RISC-V “E” 基础架构,也就是 RV32E 就是 标准嵌入式扩展 指令集(这个版本降低了核心的开销,CPU 寄存器裁剪了一半,为 16 个)。但是 FreeRTOS 和 RTT 目前支持的版本都是 32 个寄存器的,对于任务或者说线程的上下文切换时对栈帧的操作还是有一些差异。然后呢也想对比一下 ARM 架构和 RISC-V 架构下嵌入式实时操作系统处理的一些区别,这里呢就想做一些的简单记录。

ARM 和 RISC-V 架构的区别

由于我是先学的 ARM 也相对了解一些,所以做什么总是想拿来和 ARM 对比一下,看看能不能套在 ARM 上,这也对自己理解也有一些帮助。缺点就是会产生一些先入为主的观念。

一个最简单的 RTOS 应该至少要实现一个多任务管理的功能,所以 RTOS 也可以叫实时多任务操作系统。那么一个简单的 RTOS 的核心就是怎么处理多任务或者说多线程之间的切换,这里我们也叫做上下文切换,所以上下文切换机制的实现就非常重要,这就要牵扯到不同架构的 CPU 会有不同的处理方式。

ARM 架构下 RTOS 的一般处理过程

这里以 Cortex-M3 为例,在 ARM 架构中有一组 特殊功能寄存器组,很多时候就是专门留给 OS 使用的。其中由 CONTROL[0:1] 寄存器来定义 CPU 的特权等级。这里就要提到在 ARM 架构中的双堆栈机制,在 CM3 内核中支持两个堆栈,一个是 MSP(主堆栈指针)指向的主堆栈和 PSP(线程堆栈指针)指向的线程堆栈。通过配置 CONTROL 寄存器的两个位来选择特权级别和使用不同的堆栈指针(还有一个骚操作就是从异常返回时修改 LR 的 bit1bit2 也可以切换模式和堆栈,我们可以在很多开源的 RTOS 中见到)。这样通过这两个寄存器的配置就可以分开对待用户程序和系统程序,避免因用户级程序的问题对系统造成危害。同时在出入异常处理时这两个堆栈指针是通过硬件自动切换的,对于现场的保存就不需要软件来处理了。而且在 Handler 或者说异常中只能使用 MSP(主堆栈指针)。

CONTROL[0] CONTROL[1] 组合 模式
特权选择 堆栈指针选择
0 0 特权级+MSP Handler 模式和 Kernel(OS)
0 1 特权级+PSP 线程模式
1 0 用户级+MSP 错误用法
1 1 用户级+PSP 线程模式

由于有了这样的机制,在 RTOS 中对于任务切换就带来了很多便利,通常情况下都是通过 SVCall(即 SVC,System service Call,系统服务调用)PendSV(Pendable request for system serivce,可挂起系统调用)这两个异常来完成系统特权和任务上下文的切换。当然也可以先不考虑特权模式和用户模式,那么就可以仅通过 PendSV 异常来完成任务上下文切换即可。这里可以参考一下 FreeRTOS 的处理代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// SVCHandler 进行任务切换
__asm void vPortSVCHandler( void )
{
extern pxCurrentTCB; // 外部参数,当前任务控制块指针

PRESERVE8

ldr r3, = pxCurrentTCB // 加载 pxCurrentTCB 的地址到 r3
ldr r1, [r3] // 加载 pxCurrentTCB 到 r1
ldr r0, [r1] // 加载 pxCurrentTCB 指向的值到 r0, 即当前第一个任务的任务栈栈顶指针
ldmia r0!, {r4-r11} // 以 r0 为基地址,将栈里面的内容加载到 r4-r11 寄存器,同时 r0 会递增
msr psp, r0 // 将 r0 的值,即任务栈指针更新到 psp
isb
mov r0, #0 // 将 r0 的值,设置为 0
msr basepri, r0 // 将 basepri 寄存器设置为0,即所有的中断都没有被屏蔽

//骚操作
orr r14, #0x0d // 当从 SVC 中断服务退出前,通过向 R14 最后4位按位或上0x0d,
// 使得硬件在退出时,使用进程堆栈指针 PSP 完成出栈操作并返回后进入线程模式、返回 Thumb 状态
// r14 的 bit1 : 0 PSP 1 MSP;bit2: 0 特权模式 1 用户模式

bx r14 // 异常返回,这个时候栈中的剩下内容将会自动加载到 CPU 寄存器
// xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务形参) 同时 PSP 的值也将更新,即指向任务栈的栈顶
}
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
__asm void xPortPendSVHandler(void)
{
extern pxCurrentTCB; // 外部参数,当前任务控制块指针
extern vTaskSwitchContext; // 外部函数,当前任务切换函数

PRESERVE8

// 当进入 PendSVC Handler 时,上一个任务运行环境,即:
// xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务形参),这些将自动保存到任务栈中,剩下的r4-r11需要手动保存
// 获取任务栈指针到 r0
mrs r0, psp
isb

ldr r3, =pxCurrentTCB // 加载 pxCurrentTCB 的地址到 r3
ldr r2, [r3] // 加载 pxCurrentTCB 到 r2

stmdb r0!, {r4-r11} // 将 CPU 寄存器 r4-r11 的值存储到 r0 指向的地址
str r0, [r2] // 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针

stmdb sp!,{r3,r14} // 将 r3 和 r14 临时压入堆栈,因为即将调用函数
// 调用函数时,返回地址自动保存到 r14 中,导致 r14 的值会被覆盖,所以 r14 的值需要入栈保护
// r3 保存的当前激活的任务 TCB 指针( pxCurrentTCB ),函数调用后会用到,因此也需要入栈保护

mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY // 进入临界段
msr basepri, r0 // 屏蔽所有中断
dsb
isb
bl vTaskSwitchContext // 调用函数 vTaskSwitchContext,寻找新的任务运行,通过使变量 pxCurrentTCB 指向新的任务来实现任务切换
mov r0, #0 // 退出临界段
msr basepri, r0
ldmia sp!, {r3,r14} // 恢复 r3, r14

ldr r1, [r3]
ldr r0, [r1] // 当前激活的任务 TCB 第一项保存了任务堆栈的栈顶指针,现在栈顶值存入了 r0
ldmia r0!,{r4-r11} // 出栈
msr psp, r0
isb
bx r14 // 异常发生时,R14 中保存异常返回标志,包括返回后进入线程模式还是处理器模式
// 使用 psp 堆栈指针还是 msp 堆栈指针,当调用 bx r14 指令后,硬件会知道要从硬件返回
// 然后出栈,这个时候堆栈指针 psp 硬件指向了 新任务堆栈的正确位置
// 当新任务的运行地址被出栈到 pc 寄存器后,新的任务也会被执行
nop
}

这两个汇编函数就完成了 ARM 架构下的任务切换机制。其实对于任务上下文切换就是任务现场的保存和恢复,这个现场就是当前的 CPU 运行状态,也就是 CPU 各个寄存器的状态的保存与恢复。这其中也包括很重要的栈帧切换。当然仅仅靠这两个函数也是不完全可靠的,还有一些临界段的处理函数来共同保证任务的安全切换。

RISC-V 架构下的 RTOS 一般处理过程

在 RISC-V 架构中,也有不同的特权级别,目前主要定义了三种特权级别,分别是机器模式(Machine Mode,M-mode)、监管模式(Supervisor Mode,S-mode)和用户模式(User Mode,U-Mode), 通过 CSRs(control and status registers,控制状态寄存器)bit11bit12(即 MPP 位)两个位的不同编码来实现不同特权模式的切换,在不同特权模式下都有单独的 CSRs。这里需要说明的是我这个 MPP 指的是 Machine-Level CSRs 中 mstatus 寄存器(即 M-mode status register)的控制位。

Level MPP[12:11] 模式 简写
0 0 0 User/Application U
1 0 1 Supervisor S
2 1 0 Reserved(Hypervisor) (保留)
3 1 1 Machine M

但是一个 RISC-V 处理器的实现并不要求同时支持这三种特权级,接受以下的一些实现组合,降低实现成本:

Number of levels Supported Modes Intended Usage
1 M Simple embedded systems
2 M,U Secure embedded systems
3 M,S,U Systems running Unix-like operating systems

上图中可以看出,这三种模式只有 M-mode 是必须要实现的,其它两种模式是可选的。M-mode 是 RISC-V 中 hart(hardware thread,硬件线程)可以执行的最高权限模式。在 M 模式下运行的 hart 对内存,I/O 和一些对于启动和配置系统来说必要的底层功能有着完全的使用权。因此它是唯一所有标准 RISC-V 处理器都必须实现的权限模式。实际上简单的 RISC-V 微控制器仅支持 M 模式。

好了,上面说的是特权模式和 ARM 的区别,下面就是堆栈指针的区别。上文已经提到 ARM 中有 MSP 和 PSP 之分,且在 handler 中只能使用 MSP,也就意味着 OS 和线程模式使用不同的栈。并且出入异常的栈帧切换由硬件完成。

而在 RISC-V 架构处理器中,没有区分异常、中断和线程模式使用的栈帧,在进入和退出中断处理模式时没有硬件自动保存和恢复上下文(通用寄存器)的操作,因此需要软件明确地使用(汇编语言编写的)指令进行上下文的保存和恢复。并且还要区分 ecall(environment call for U/S/M-mode,不同特权模式下的环境调用异常)。

所以 RISC-V 这一块的处理要复杂一些,有大量的 RISC-V 汇编,具体的代码我就不贴了,有兴趣的可以去看一下 FreeRTOS 的源码。链接:https://github.com/FreeRTOS/FreeRTOS-Kernel/blob/master/portable/GCC/RISC-V/portASM.s

下表是 RISC-V RV32I 基础指令集寄存器结构,但 RV32E 基础指令集只有 x0-x15

Register ABI Name Description Saver
x0 zero Hard-wired zero -
x1 ra Return address Caller
x2 sp Stack pointer Callee
x3 gp Global pointer -
x4 tp Thread pointer -
x5-7 t0-2 Temporaries Caller
x8 s0/fp Saved register/Frame pointer Callee
x9 s1 Saved register Callee
x10-11 a0-1 Function Arguments/return values Caller
x12-17 a2-7 Function arguments Caller
x18-27 s2-11 Saved registers Callee
x28-31 t3-6 Temporaries Caller

上表中虽然对各个寄存器有了一些描述,在 RISC-V 指令集中并没有指定专用的堆栈指针或子程序返回地址链接寄存器等,事实上指令编码允许将任何 x 寄存器用于这些目的。 但是,标准软件调用约定使用寄存器 x1 来保存呼叫的返回地址,而寄存器 x5 可用作备用链接寄存器。 标准调用约定使用寄存器 x2 作为堆栈指针。硬件可能会选择加速使用 x1 或 x5 的函数调用和返回。(不知道这段 Google 翻译的描述是否准确,大家可以去阅读《riscv-spec-20191213》的 2.1 节原文参考)

在 wujian100 RISC-V 开源平台上实现简单的任务调度系统

在了解了上面的一些区别后,我准备尝试移植 FreeRTOS 或者 RT-Thread 到 wujian100 上试试,但是我发现它们大多是只支持了以 RV32I 为基础指令集的处理器。而 wujian100 是 E902,是 RV32E 基础指令集,在底层汇编的处理上有一些不同,可能还要做一些修改。所以我就想试着把我之前学习 FreeRTOS 时,实现的仅有任务调度功能的极简版 FreeRTOS 放上去试试,因为代码量比较少。

接下来,我就尝试在 wujian100 开源的 SDK 中移花接木,把我自己这个极简的小操作系统移植上去。在仔细翻阅了 wujian 开源的代码后发现他们这里提供了一个 AliOS 的内核,叫 rhino 内核。在他们这个内核的底层是有实现一些上下文切换的代码的,于是我就基于这个底层把我的上层接上去。当然这过程中还要修改很多东西,这里就不一一详述,直接看这段汇编代码是怎么处理的,这里我已经做了一些修改,我的两个小任务也转起来了。

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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#define MSTATUS_PRV1 0x1880

.global cpu_intrpt_save
.type cpu_intrpt_save, %function
cpu_intrpt_save:
csrr a0, mstatus // 读控制状态寄存器,写入 a0,并返回到 psr 返回值中,psr 是外部定义的一个变量,恢复时会使用
csrc mstatus, 8 // 将控制状态寄存器清零。清零对应的标志位,该语句即为清除 MIE ,即禁止全局中断使能。就是禁用中断
ret

.global cpu_intrpt_restore
.type cpu_intrpt_restore, %function
cpu_intrpt_restore:
csrw mstatus, a0 // a0 是传进来的参数,即上一次保存的控制状态寄存器的值,对于 a0 中每一个为 1 的位,把 mstatus 中对应的位进行置位
ret

.global cpu_task_switch
.type cpu_task_switch, %function
cpu_task_switch: // 主动任务切换调度
la a0, g_intrpt_level_1 // g_intrpt_level_1 是一个全局变量,用于保存当前中断嵌套的层级;这里是将其地址加载到 a0 中
lb a0, (a0) // 将 a0 地址的数据加载到 a0 中
beqz a0, __task_switch // beqz 是对于零时的分支指令,如果等于零,就执行 __task_switch 函数,也就是意味着当前没有中断嵌套

la a0, pxCurrentTCB // 如果不等于零,即有中断嵌套,就进行下面的操作;加载 pxCurrentTCB 的地址到 a0,即获取当前任务指针
la a1, g_ReadyTasksLists // 加载 g_ReadyTasksLists 的地址到 a1,即获取当前最高优先级的就绪任务指针
lw a2, (a1) // 加载就绪任务指针到 a2 (lw 指令读取一个字,即4个字节的数据 到 a2
sw a2, (a0) // 将 a2 的低4个字节存储到 a0(即将就绪任务指针放到当前任务)

ret

.global cpu_intrpt_switch
.type cpu_intrpt_switch, %function
cpu_intrpt_switch: // 中断中的任务切换 操作和上面类似
la a0, pxCurrentTCB
la a1, g_ReadyTasksLists
lw a2, (a1)
sw a2, (a0)

ret

.global cpu_first_task_start
.type cpu_first_task_start, %function
cpu_first_task_start: // 第一次进入任务时是不用返回的
j __task_switch_nosave

.type __task_switch, %function
__task_switch: // 任务切换函数
addi sp, sp, -60 // 规划保存数据需要的栈帧大小
// 保存现场,将寄存器的数据保存到栈帧中
sw x1, 0(sp)
sw x3, 4(sp)
sw x4, 8(sp)
sw x5, 12(sp)
sw x6, 16(sp)
sw x7, 20(sp)
sw x8, 24(sp)
sw x9, 28(sp)
sw x10, 32(sp)
sw x11, 36(sp)
sw x12, 40(sp)
sw x13, 44(sp)
sw x14, 48(sp)
sw x15, 52(sp)

sw ra, 56(sp)

la a1, pxCurrentTCB // 将当前任务控制块指针地址,加载到 a1
lw a1, (a1) // 将任务控制块指针地址加载到 a1
sw sp, (a1) // 将栈指针加载到当前任务控制块指针地址

__task_switch_nosave: // 第一次进入任务入口,接下来切换任务指针
la a0, g_ReadyTasksLists
la a1, pxCurrentTCB
lw a2, (a0)
sw a2, (a1)

lw sp, (a2)

/* Run in machine mode */
li t0, MSTATUS_PRV1
csrs mstatus, t0 // 将对于 t0 对应为 1 的每一位置位,即 mpp 设置为 11,machine mode 运行;mpie 置位,用于保存发生异常时 mie 的值;即切换到 M-mode

lw t0, 56(sp) // 将 56(sp) 低 4个字节的数据加载到 t0,即返回地址
csrw mepc, t0 // 将 t0 写入 mepc 这里需要注意的是,栈区的数据,在任务初始化的时候就要初始化好,包括第一次启动
// 加载栈帧数据
lw x1, 0(sp)
lw x3, 4(sp)
lw x4, 8(sp)
lw x5, 12(sp)
lw x6, 16(sp)
lw x7, 20(sp)
lw x8, 24(sp)
lw x9, 28(sp)
lw x10, 32(sp)
lw x11, 36(sp)
lw x12, 40(sp)
lw x13, 44(sp)
lw x14, 48(sp)
lw x15, 52(sp)

addi sp, sp, 60
mret // M-mode 特有指令,返回时将 PC 指针设置为 mepc,将 mpie 复制到 mie 恢复之前的中断设置,并将特权模式设置为 mpp 中的值;这里就可以完成特权模式的切换(M-U or U-M)

.global Default_IRQHandler
.type Default_IRQHandler, %function
Default_IRQHandler: // 异常、中断处理,这里也需要保存现场,处理类似
addi sp, sp, -60

sw x1, 0(sp)
sw x3, 4(sp)
sw x4, 8(sp)
sw x5, 12(sp)
sw x6, 16(sp)
sw x7, 20(sp)
sw x8, 24(sp)
sw x9, 28(sp)
sw x10, 32(sp)
sw x11, 36(sp)
sw x12, 40(sp)
sw x13, 44(sp)
sw x14, 48(sp)
sw x15, 52(sp)

csrr t0, mepc
sw t0, 56(sp)

la a0, pxCurrentTCB
lw a0, (a0)
sw sp, (a0)

la sp, g_top_irqstack

csrr a0, mcause // 读取异常类型
andi a0, a0, 0x3FF
slli a0, a0, 2
// 处理异常
la a1, g_irqvector
add a1, a1, a0
lw a2, (a1)
jalr a2
// 退出异常,恢复
la a0, pxCurrentTCB
lw a0, (a0)
lw sp, (a0)

csrr a0, mcause
andi a0, a0, 0x3FF

/* clear pending *///清除挂起的异常
li a2, 0xE000E100
add a2, a2, a0
lb a3, 0(a2)
li a4, 1
not a4, a4
and a5, a4, a3
sb a5, 0(a2)

/* Run in machine mode */
li t0, MSTATUS_PRV1
csrs mstatus, t0

lw t0, 56(sp)
csrw mepc, t0

lw x1, 0(sp)
lw x3, 4(sp)
lw x4, 8(sp)
lw x5, 12(sp)
lw x6, 16(sp)
lw x7, 20(sp)
lw x8, 24(sp)
lw x9, 28(sp)
lw x10, 32(sp)
lw x11, 36(sp)
lw x12, 40(sp)
lw x13, 44(sp)
lw x14, 48(sp)
lw x15, 52(sp)

addi sp, sp, 60
mret

除了上面的汇编部分,还有几个主要函数如下,代码工程我后面整理好会上传到我的 Github 上。

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

void TaskSwitching_example(void)
{
prvInitTaskLists();

Task1_Handle = xTaskCreateStatic( Task1_Entry,
"Task1_Entry",
TASK1_STACK_SIZE,
NULL,
1,
Task1Stack,
&Task1TCB );
// 核心就是插入函数 vListInsert, 将任务插入到就绪列表中
vListInsert(&pxReadyTasksLists[1], &Task1TCB.xStateListNode);

Task2_Handle = xTaskCreateStatic( Task2_Entry,
"Task2_Entry",
TASK2_STACK_SIZE,
NULL,
2,
Task2Stack,
&Task2TCB );
vListInsert(&pxReadyTasksLists[2], &Task2TCB.xStateListNode);
vTaskStartScheduler(); //去启动第一个任务
}

void vTaskSwitchContext(void)
{
// 轮流切换两个任务,我这里任务暂时是手动切换的,没使用优先级
if( pxCurrentTCB == &Task1TCB)
{
g_ReadyTasksLists[0] =& Task2TCB;
}
else
{
g_ReadyTasksLists[0] =& Task1TCB;
}
}

void wjYIELD(void)
{
PSRC_ALL();
portDISABLE_INTERRUPTS();
vTaskSwitchContext();
cpu_task_switch();
portENABLE_INTERRUPTS();
}

// 第一个任务函数 Task1 入口函数 ;task2 和 task1 一样
void Task1_Entry(void *p_arg)
{
for(;;)
{
flag1 = 1;
printf("flag1 = %d \n", flag1);
delay( 100 );
// vTaskDelay( 20 );
flag1 = 0;
printf("flag1 = %d \n", flag1);
delay( 100 );
// vTaskDelay( 20 );
wjYIELD(); // 注意,这里是手动切换任务
}
}

int main(void)
{
TaskSwitching_example();
return 0;
}

好了,差不多就这些了。
wujian_CamelOS

踩坑总结

通过这次研究,明白了 ARM 和 RISC-V 架构上的异同,加深了自己对两种架构的理解,相信对以后的学习也更加有帮助。
还有就是 wujian100 的开源资料中并没有提供特权架构的相关文档,异常和中断向量表规划也没有具体的说明文档,目前有限的文档中只介绍了外设 IP 的说明,所以在后续的软件开发增加了很多障碍。只有去扒他们提供的 SDK 中的源代码,通过源码来了解他们的架构,还有一点就是他们提供的代码及资料和阿里体系的东西相对耦合或者说兼容。跟开源社区现有的资料和体系不能很好融合。

参考资料

🔲 ☆

Windows10 环境下搭建 RISC-V 调试环境

环境要求

软件环境

硬件要求

  • 目标 RISC-V 芯片
  • 调试器: J-Link,FT2232 或其他含有标准 JTAG 接口的调试器

配置环境

以下内容来自 ChenRQ 同学!

启动 IDE: GNU MCU Eclipse IDE for C/C++ Developers,Eclipse 基于 Java 开发,运行时需要 Java 的运行环境(JRE),如没有请自行安装。

新建一个工程

新建工程

工程类型选择 Hello World RISC-V C Project,工具链选择 RISC-V Cross GCC 如下所示

项目类型设置

使用默认配置 next 至 GNU 工具链选择, 文件路径应指向为您的 RISC-V Embedded GCC 目录下的 bin 文件夹,如下图所示

工具链路径设置

完成后点击 Finish 由此完成工程项目的创建。创建完成后,我们可以看到还有一个报错, 如下图所示

工程创建完成界面

因此我们还需要继续对项目进行配置。

工程相关配置

对工程右键选择 “properties”,在 MCU 选栏中配置 Build Tools Path,该路径应指向您的 Build Tools 目录下的 bin 文件夹,如下图所示

Build Tools 路径配置

继续配置 OpenOCD Path,路径为 OpenOCD 目录下的 bin 文件夹,如下图所示,并点击apply

OpenOCD 路径配置

再配置 RISC-V Toolchain Path(若新建项目时已配置过工具链路径,可以跳过此步骤),配置路径与工程建立时选择的工具链路径相同。

配置编译和链接选项

继续在 “properties” 窗口中,选择 C/C++ Build 中的 settings,在 Tool Settings 中 Target Processor 进行配置,由于是 RISC-V,因此架构 (architecture) 选择 RV32I 并勾选乘法指令拓展(RVM),原子指令拓展(RVA)及压缩指令拓展(RVC),ABI 调用选择 ILP32(表明为 32 位架构无浮点型,PS: ilp32f 和 ilp32d 则分别表示单精度浮点和双精度浮点),Code Model 选择 Medium Low,勾选整数除法指令(-mdiv),如下图所示,并点击 apply

Target Processor 配置

继续配置 Optimization,Level 选择 -O2,如下图所示,并点击 apply

Optimization 配置

继续配置 Debugging,Level 选择 -g,如下图所示,并点击 apply

Debugging 配置

在 Tool Settings 中选择 GNU RISC-V Cross C Linker 的 General,点击右上角+号,弹窗中选择 Workspace 选择路径您芯片对应的 lds 文件,用于对地址区间进行约束,如下图所示

链接脚本配置

勾选对应选项后,点击 apply,如下图所示

链接选项配置

在 Tool Settings 中选择 GNU RISC-V Cross C Linker 的 Miscellaneous 进行勾选,如下图所示,并点击 apply

链接杂项配置

添加您的工程汇编类型的头文件路径,方法如下图所示

添加工程汇编头文件目录

添加您的工程 C/C++ 类型的头文件路径,方法如下图所示

 添加工程 C/C++ 头文件目录

待续…

后续测试进行中…

🔲 ☆

使用汇编实现 pc 和 sp 的保存及恢复操作

前言

在 ARM Cortex 系列的芯片中本来就有一套保护现场的机制,例如当产生了一个中断时,会自动将当前寄存器的值入栈,并在 lr(r14) 寄存器中保存将要返回的 pc 值,在中断服务程序执行完成后将 pc 恢复到之前的位置。如果在执行中断服务程序的时候又发生了优先级更高的中断,也就是说发生了中断嵌套,这是将再次进行现场保护,同时 lr 值会被压栈(上一次的 pc ),新的 lr 生成。

但是在一些场景下,这样的机制就不太好用了,比如说要进入 sleep 模式 cpu 掉电了,想要恢复到掉电前的状态。这样的话就需要我们自己实现保护现场了,下面就来简单介绍一下我的实现。

硬件及 IDE 环境

  • 硬件: Cortex-M3 FPGA 开发板
  • IDE: IAR 8.22.1

在进行 FPGA 验证之前,还跑了 RTL 的仿真,从仿真波形的结果来看也是正确的。

c 文件

现场保护主要就是保存当前的运行状态,在从 sleep 模式唤醒后将保存的状态恢复,使 cpu 回到到 sleep 之前的状态。在我们这里最主要的是保存 pc 和 sp 的值,cpu 唤醒之后恢复 pc 和 sp 就好,所以我们需要将进入 sleep 之前的 pc 和 sp 保存即可。

在进入 sleep 模式中,虽然 cpu 掉电了,但是 SRAM 还是维持着的,所以我们可以使用一个全局变量(存储在 SRAM 中)来保存 pc 和 sp 的值。

“xxx.c” 文件中部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 函数及变量的声明和引用
extern void Save_PC_SP(void)
extern void Restore_PC_SP(void)
extern u32 pc_save;
extern u32 sp_save;

//······
// 在执行 sleep 指令(WFI/WFE)之前保存 pc、sp
Save_PC_SP(); // 保存 pc 和 sp
__WFI(); // 睡眠
__NOP();
__NOP();
__NOP();

//······

s 汇编文件

“xxx.s”文件中的部分代码:

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
;函数及变量的声明和引用
PUBLIC Save_PC_SP
PUBLIC Restore_PC_SP

IMPORT pc_save
IMPORT sp_save

;唤醒后判断的代码
THUMB

PUBWEAK Reset_Handler
SECTION .text:CODE:REORDER:NOROOT(2)
Reset_Handler
LDR R0, =0x4001f000
LDR R1, [R0]
CMP R1, #1
BEQ __iar_program_start
B Restore_PC_SP


;Save pc sp 的代码

SECTION .text:CODE:NOROOT
Save_PC_SP
LDR R0, =0x4001f000
MOV R1, #1
STR R1, [R0]
LDR R0, =sp_save
LDR R2, =pc_save
MOV R1, R13
STR R1, [R0]
MOV R1, LR
STR R1, [R2]
BX LR


;Restore pc sp 的代码

SECTION .text:CODE:NOROOT
Restore_PC_SP
LDR R0, =sp_save
LDR R1, [R0]
MOV R13, R1
LDR R0, =pc_save
LDR R1, [R0]
ADD R1, R1, #0x8
MOV PC, R1
NOP
NOP
NOP

在汇编文件中主要实现的是 save 和 restore 的操作,以及恢复过程的判断。因为我们的设计是从睡眠唤醒是从 Reset 起来的,这就导致第一次 cpu 的正常启动会和 restore 发生冲突,所以我这里选择了一个不会掉电的寄存器来作为是否进行 restore 的判断。

还有就是加 NOP 指令是因为 Cortex-M3 是三级流水线,为了防止 cpu 因为 pc 的预取而发生错误。

❌