Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tock 源码相关 #840

Open
cisen opened this issue Feb 9, 2020 · 8 comments
Open

tock 源码相关 #840

cisen opened this issue Feb 9, 2020 · 8 comments

Comments

@cisen
Copy link
Owner

cisen commented Feb 9, 2020

总结

Core options
NVIC Nested Vector Interrupt Controller 37 vectors
PRIORITIES Priority bits 3
WIC Wakeup Interrupt Controller NO
Endianness Memory system endianness Little endian
Bit Banding Bit banded memory NO
DWT Data Watchpoint and Trace YES
SysTick System tick timer YES
Modules
MPU Memory protection unit YES
FPU Floating point unit YES
DAP Debug Access Port YES
ETM Embedded Trace Macrocell YES
ITM Instrumentation Trace Macrocell YES
TPIU Trace Port Interface Unit YES
ETB Embedded Trace Buffer NO
FPB Flash Patch and Breakpoint Unit YES
HTM AHB Trace Macrocell NO
let ipc = kernel::ipc::IPC::new(board_kernel, &memory_allocation_capability);

    let launchxl = Platform {
        console,
        gpio,
        led,
        button,
        alarm,
        rng,
        i2c_master,
        ipc,
    };

    let chip = static_init!(cc26x2::chip::Cc26X2, cc26x2::chip::Cc26X2::new(HFREQ));

    extern "C" {
        /// Beginning of the ROM region containing app images.
        static _sapps: u8;
    }

    kernel::procs::load_processes(
        board_kernel,
        chip,
        &_sapps as *const u8,
        &mut APP_MEMORY,
        &mut PROCESSES,
        FAULT_RESPONSE,
        &process_management_capability,
    );

    board_kernel.kernel_loop(&launchxl, chip, Some(&launchxl.ipc), &main_loop_capability);

流程

  • 用rust/c编写app代码,申请堆/驱动等需要掉tock api,rust只有栈,只需要sp指针即可
  • 使用(链接脚本)[https://github.com/GNU Linker Script(.lds文件/链接脚本)相关 #858]描述芯片的flash和内存
  • .cargo/config配置链接脚本后,生成elf文件,然后再用elf2tab转化为tab格式
  • 按下单片机电源后,cpu就会从flash 0地址位置加载系统程序,系统初始化硬件后就会开始系统循环
  • 系统循环会查找flash上是否有已安装的app,有则加载tab文件并解析,计算出app sp指针/栈的位置,堆的位置和预留的系统空间(驱动通信)
  • 计算完就使用MPU设置内存的权限
  • 创建完app进程即可系统循环并进入app进程的callback执行用户代码

编译安装

raspberry

  • sudo apt install rust-lldb

architecture

process_memory_layout

processram

tock-stack

问答

文件系统在哪里?

  • capsules\src\sdcard.rs提供对sd卡的读写

没有线程?

  • 搜索不到thread关键字
  • 没有只有单进程

进程是单线程的?

  • 是的,一般单片机都是单线程的

如何跟底层硬件链接?

  • arch文件夹里有各个cpu的汇编实现基础函数
  • rust通过asm!宏来调用汇编

什么叫MPU?

  • 内存保护单元(ARM体系方面)(MPU,Memory Protection Unit),MPU中一个域就是一些属性值及其对应的一片内存。这些属性包括:起始地址、长度、读写权限以及缓存等。
  • MPU (Microprocessor Unit)微处理器 微机中的中央处理器(CPU)称为微处理器(MPU)

为什么回调比闭包好?又什么区别?

进程的回调是什么?为什么是这种结构?

  • kernel\src\process.rs FunctionCall

进程是如何到指令的?

  1. app会被rust打包成elf文件(此时代码已经被编译为对应cpu的机器diamond)
  2. 使用elftab转换为tab文件
  3. 使用tockloader根据主板安装进各个板子指定的内存地址
  4. 启动系统,读取app的头,识别.txt,.data等执行对应的指令并执行

kernel是什么?代码在哪里?

  • 就是sched,代码在kernel\src\sched.rs

kernel是先创建process在创建kernel?

  • kernel也是一个process
  1. tock需要根据主板编译适配的系统,也就是bin文件,这个文件是可以直接在cpu执行的。
  2. 烧录的时候,根据各个主板地址规则和链接脚本,一般0x0000就是中断地址,将中断入口烧入内存。中断向量表一般在chips\sam4l\src\lib.rs
  3. 触发关启机时就会调用中断向量表的reset_handler,
  4. reset_handler会做:
    1. 初始化主板,比如nrf52832::init()
    1. 初始化kernel,加载各个硬件的capsule驱动为component
    1. 初始化I2C, UART, RTC等一些底层协议
    1. 加载已烧录的app
    1. 开始kernel_loop,内核不断循环

capsules是什么?

  • 是驱动的统一抽象,各个硬件根据自身硬件实例化生成各个硬件capsules

进程在哪里创建和初始化的?

  • 创建在kernel\src\process.rs的load_processes函数创建线程
  • 初始化是在kernel\src\process.rscrate unsafe fn create(函数里面
  • 编译tock系统的时候在boards下面会有对应的chip_layout声明prog (rx) : ORIGIN = 0x00030000, LENGTH = 0x00010000应用程序的安装地址
  • 安装app的时候,都是使用tockloader,tockloader会声明所有boards的apps_start_address程序的安装位置,然后就可以使用openocd将app烧录进对应从内存地址
  • 启动系统加载进程,在load_processes前面会从链接脚本中捞出app的安装地址_sapps,读取固定长度的tbf头,找到app的体积,然后遍历安装所有的app
  • _sapps是这样跟prog关联的:
/* STATIC ELEMENTS FOR TOCK APPLICATIONS */
    .apps :
    {
        /* _sapps symbol used by tock to look for first application */
        . = ALIGN(4);
        s = .;

        /* Optional .app sections a convenience mechanism to bundle tock
           kernel and apps into a single image */
        KEEP (*(.app.*))
    } > prog

多线程是如何调用多核cpu的?

  • cpuid,ScbRegisters
  • 目前仅支持单核mcu,不支持64位cpu

线程包含哪些必要的内存参数?

线程是如何切换的?

  • 没有线程切换只有单进程

创建一个进程需要什么?

  • boards\components有各种硬件基础组件的抽象,硬件线程的初始化都是从这里开始的
  • component是对capsules的简单封装,capsules才是硬件线程的通用抽象
  • capsules是通过调用boarder传入的gpio来调用硬件的
  • boarder的gpio是chip的GPIOPin,而GPIOPin对kernel的GPIO的实例。chips实现的GPIOPin又会使用通用的cpu类型arch库。所以
kernel(GPIO)  ->  chip/arch(GPIOPin)  -> capsules(GPIO)  ->  components -> boards/components

每个硬件比如led都占用一个线程?

  • 不是,每个硬件只是capsule实例化,然后传入process给process去订阅使用

现在每块板会创建多少个进程?

  • 各个不同,不过都是固定数量的进程,
  • ipc: kernel::ipc::IPC::new(board_kernel, &memory_allocation_capability),只有这里创建了跟内核通信的ipc

进程的数量是固定的,app是不固定的?

  • 是的,会遍历进程的数量,然后将app数据从内存中读取出来初始化进程
  • 实现在load_processes函数

如何确定一段内存里面有几个app?

  • 循环创建进程的时候,可以获得memory_offset/app_flash_size,得到每个app内存的结束位置,在Process::create函数里面

syscall做什么的?有什么用?

  • 系统调用,调用系统的api

systick有什么用?怎么用?

  • systick可以以cpu的频率调用一个函数
  • systick提供一个可以固定频率产生中断的硬件接口
  • 用法是
    • 先通过链接脚本往ISR_VECTOR注入systick_handler
    • 然后初始化systick, 并且设置频率
    • 这样systick就会以固定的频率产生中断调用systick_handler来切换进程
  • 详见preemptive

tock系统是如何注入systick_handler的?

  • 是在chips里面注入BASE_VECTORS

arch下面的SysTick有什么用?如何跟systick_handler关联

  • 是kernel下面的systick实现,用于设置和读取systick的时间
  • systick时间到了就会调用systick_handler切换进程,而时间是SysTick设置的

如何将systick和process联合在一起?

  • 通过切换SP指针就能切换kernel和process的进程,systick就是通过改变sp指针来切换进程的
  • 一般systick是用于进程超时,需要将进程切换到内核进程

Cortex-M3 事实上有两个栈指针寄存器,分别为:

  • 主栈指针寄存器 MSP (Main Stack Pointer),用于 OS 内核进程。
  • 进程指针寄存器 PSP (Process Stack Pointer),用于应用进程。

它们可以分别存储不同的值,且可以通过寄存器名 MSP 和 PSP 直接读写。寄存器名 SP 指代其中哪一个取决于当前 CONTROL 寄存器的 bit[1]。

  • CONTROL[1]=0 选择主堆栈指针
  • CONTROL[1]=1 选择进程堆栈指针
    通过切换 CONTROL 寄存器的值我们可以轻松地在内核与应用间切换调用栈。通常我们不会直接读写 CONTROL 寄存器,而是使用不同的中断返回地址来切换调用栈。(这是因为如果开启了特权保护,用户进程无法使用 MRS 以及 MSR 指令,进而无法读写 CONTROL,只能通过中断提权)

tock是如何注入app?

app的开发流程是什么?

  1. 借助libtock引入timer/led等硬件驱动编写程序,如果不需要调硬件也可以不引入libtock-rs
  2. 编写完使用libtock-rs写好的make脚本编译出.tab文件,实际上还是使用cargo build --release --target=thumbv7em-none-eabi构建出应用(就是.bin文件,只是没带后缀),然后使用elf2tab转化为tab文件
  3. 使用libtock-rs自带的make将app 烧录进主板,或者tockloader也可以将tab文件烧录进主板

系统的启动/关闭/重启是如何在哪里实现的?

  • 通过中断向量表
    • 往各种chip注入的BASE_VECTORS注入reset_handler
    • 触发重启中断时,会调用各个border上写的reset_handler,来初始化kernel,加载驱动capsule,加载进程process,开始kernel循环

tock能否通过不重启来加载和启动app?就是添加一个flash进程把app写入flash

  • 不行,目前只有单进程

目前实现了哪些协议?

  • ipv6,thread,udp

MUC内部实现了数据链路层?

  • 目前仅仅实现了IEEE 802.15.4 radio部分协议,和上层的ip/thread/6lowpan/udp
  • 只有Nordic的板子支持
  • Mac层使用的是CSMA/CA协议

IEEE802154的实现原理是什么?

TASKS_TXEN 0x000 Enable RADIO in TX mode  
TASKS_RXEN 0x004 Enable RADIO in RX mode  
TASKS_START 0x008 Start RADIO  
TASKS_STOP 0x00C Stop RADIO  
TASKS_DISABLE 0x010 Disable RADIO
  • 封装一个radio对象,使用EasyDMA来操作协议缓存,使用PPI来接受radio信息
  • 是直接通过控制射频信号来实现的,比如搜索 TXADDRESS, RXADDRESSES RXMATCH

IP协议是如何调用数据链路层的?

网络调用的原理是什么?

  • 通过修改寄存器和PPI/缓存编写调用各个芯片射频接口,实现数据传递,芯片内完成数模转化获得数据
  • 获得的数据可以根据各个协议比如IEEE802154实现mac/CSMA/ip等协议

tock是如何调用cortex的MAC层进行通信的?

  • 使用了X-MAC协议,X-MAC减小了数据串扰的带来的能量消耗

IEEE 802.15.4相关

  • IEEE 802.15.4只规定了物理层和MAC层,所以TOCK要实现IEEE 802.15.4必须是自己调用射频发射器(radio)
  • 射频的定义是各自芯片决定的,但是芯片都必须声明自己的硬件满足IEEE 802.15.4的要求
  • 目前也只有NRF52系列有满足IEEE 802.15.4的硬件和驱动,所以TOCK实现了这个

IPC通信的原理是什么?

  • 每个进程都只有一个IPC服务,内含三个关键结构
    • shared_memory,是我线程传给订阅者的结果,通过allow函数修改,订阅者通过schedule_callback函数获取
    • client_callbacks,是我进程订阅其他进程的回调,调subscribe的时候就会往client_callbacks塞入订阅函数,我进程调schedule_callback就会执行订阅的回调并通过被订阅的shared_memory获得结果
    • callback,是其他进程告诉我,我订阅的进程出结果了,赶紧去执行schedule_callback
  • 都是通过appid去获得各个线程的数据。当前执行的进程可以通过kernel去获得其他进程的shared_memory数据

用户进程是如何跟内核进程通信的?

  • 通过每个进程内的shared_memory将结果交付给订阅者,订阅者通过kernel进入被订阅的进程的shared_memory

普通进程是如何订阅硬件的?比如button

  • capsule自己内部实现了subscribe和command,跟IPC的不一样

进程死循环怎么处理?

  • 有systick,定时中断切换进程

如何知道硬件button按下了?

  • 触发中断
  • 通过command订阅button的事件
  • 通过NVIC 中断向量表捕获中断
  • 主要实现在arch\cortex-m\src\nvic.rs,还有比如:chips\nrf52\src\interrupt_service.rschips\cc26x2\src\gpio.rs

hard_fault是什么?

  • 硬件错误导致的中断,可能是内存不对其或者空指针访问

NVIC是在哪里跟GPIO绑在一起的?

  • 比如chips\sam4l\src\chip.rs文件里的service_pending_interrupts函数
    • 先通过match找到要打的中断的处理函数
    • 然后创建中断cortexm4::nvic::Nvic::new(interrupt)
    • 最后通过arch\cortex-m\src\nvic.rsn.enable()往寄存器中填入处理函数地址,就完成了中断侦听
  • 具体流程是在chips\stm32f4xx\src\chip.rsservice_pending_interrupts函数
    • 遍历所有的中断类型,找到对应的回调函数执行
    • 执行完删掉该回到函数
    • 重新将该回调函数注册到中断向量表

第一次中断时如何插入的?

  • 所有的中断都是会调用同一个函数,下面是插入中断回调函数
#[link_section = ".vectors"]
#[used] // Ensures that the symbol is kept until the final binary
pub static IRQS: [unsafe extern "C" fn(); 80] = [generic_isr; 80];
  • 所有的中断的回调都是:
// arch\cortex-m0\src\lib.rs
pub unsafe extern "C" fn generic_isr() {

GPIO的中断是在哪里开始执行的?

  • 所有的中断的回调都是:
// arch\cortex-m0\src\lib.rs
pub unsafe extern "C" fn generic_isr() {
  • 执行完上面的,中断的信息就会存到数据中
  • 然后进入kernel循环,通过service_pending_interrupts重新获取中断信息并执行中断回调

nrf52的nvic在哪里?

  • chips\nrf52\src\nvmc.rs

什么是VolatileCell?

  • 文档提升在chips/sam4l/src
  • 翻译是易变的,是随时可以改变值?
  • 跟中断相关,比如pub unsafe extern "C" fn generic_isr() {函数的最后就有这个单词

fired的实现在哪里?

  • capsules\src\gpio.rs

fired调用的schedule函数在哪里?

  • kernel\src\callback.rs

fired的执行流程是什么?

  • 中断触发handle_interrupt函数,然后调用fired
  • fired(capsules\src\gpio.rs)通过kernel找到自己的进程,然后将callback回调插入进程的回调队列中

GPIO中断的执行流程是什么?

  • 先通过pub static IRQS: [unsafe extern "C" fn(); 80] = [generic_isr; 80];往芯片插入generic_isr中断函数
  • 在通过各个capsule的Driver的subscribe函数将回调插入Driver中保存
  • pub unsafe extern "C" fn generic_isr() {函数会保存中断信息,停止中断,继续kernel循环
  • kernel_loop不断通过service_pending_interrupts查找是否有中断
  • 查找到中断遍历找到对应的handle_interrupt函数
  • handle_interrupt函数通过fired,先找到kernel对象,然后再通过kernel对象找到对应的进程,把该进程存储到driver里面的回调插入process的回调中
  • kernel循环遍历完中断开始遍历process,process执行完后开始执行驱动插入的回调

上面的中断如何跟drive的subscribe关联的?

  • process通过subscribe将回调存入驱动driver中
  • 发生中断driver调用handle_interrupt时,会将自己身上callback重新插入process中,然后kernel循环到进程时会执行callback

如何知道触发的中断类型的?

中断的初始化流程

  • generic_isrBASE_VECTORS写入boards\kernel_layout.ldvectorsROM中
  • 中断触发reset_handle,然后pub unsafe fn init开始读取ROM的数据到RAM

zero_bss是做什么的?

  • GNU Linker Script(.lds文件/链接脚本)相关 #858
  • bss段(未手动初始化的数据)并不给该段的数据分配空间,只是记录数据所需空间的大小。.bss是不占用.exe文件空间的,其内容由操作系统初始化(清零);而.data却需要占用,其内容由程序初始化
  • data(已手动初始化的数据)段则为数据分配空间,数据保存在目标文件中。

内存布局时如何确定的?

  • 由各个主板的layout.ld确定自己的内存大小,由统一的kernel_layout布局系统内存
  • 编译器编译结果的内存使用也是由操作系统定义的?就是说用户定义的变量,编译的时候就会分配好内存地址给你,动态的地址也是在固定的内存上面增加的

如何读取和识别elf/tab文件的?

为什么要编译为机器代码而不是执行?

  • 实际过程中都是先生成通用的汇编,然后再将汇编生成机器代码。
  • 高级语言直接生成机器代码太复杂,太多映射,重复劳动太多
  • CPU只能识别机器码,汇编最后也是经过编译器生成机器码才能运行的

ids是如何被rust读取和解析的?

  • ids文件时在.cargo\config

tockloader是如何安装程序的?

tock的程序不用在该系统里面编译?

  • 是的,可以在外部使用libtock-rs,实现PIC的程序

tock的内存分页是如何做的?

  • 每个硬件的PAGE_SIZE大小不一样的,自定义page映射的方法在kernel\src\hil\flash.rs
  • 分页控制在capsules\src\nonvolatile_to_pages.rs?

tock写入页的流程是什么?

  1. 具体实现在chips\sam4l\src\flashcalw.rs,入口时write_page函数
  2. write_page函数复制缓存一份将要被写的数据到self.buffer,修改状态为WriteUnlocking(开始写操作),然后执行lock_page_region触发中断
  3. 执行中断函数handle_interrupt,匹配到状态WriteUnlocking,触发WriteErasing进行清空那一页的数据,并继续触发中断
  4. 中断匹配到WriteErasing,执行write_to_page_buffer将page的数据写入缓存中,写的实现是找到page的地址,然后进行指针偏移复制。设置状态为WriteWriting,并触发写入保护中断
  5. 中断匹配到WriteWriting,说明已经写完,清空缓存,设置状态为Ready,并执行写完成的kernel flash 回调。完成写操作
    注意的是:
  • 这里的写都是根据PAGE_SIZE一次写入的,也就是根据PAGE_SIZE对齐的
  • flash的中断注入是在nvic::HFLASHC => flashcalw::FLASH_CONTROLLER.handle_interrupt(),

capsules\src\nonvolatile_to_pages.rs和capsules\src\nonvolatile_storage_driver.rs的功能分别是什么?

  • nonvolatile_to_pages是将内存分页读写的控制器,也是暴露给用户去使用的,跟nonvolatile_storage_driver关系不大
  • nonvolatile_storage_driver是暴露给app和内核操作RAM的接口,采用发布订阅的方式

内核的页目录在哪里?

  • kernel\src\process.rsmemory: &'static mut [u8],?

内核是如何分配内存给各个app的?

  • kernel\src\memop.rs统一进程的内存操作码,内存的控制仍然在kernel\src\sched.rs内核的processes指针
  • 为各个进程分配内存的是在kernel\src\process.rs的create函数里面,通过kernel的process全局app内存指针和tbf文件的数据,分配出app的内存。关键分配还是在创建进程这里

安装app和安装内核的openocd命令有何不同?

  • 内核的:boards/launchxl/flash-kernel.openocd
flash write_image erase target/thumbv7em-none-eabi/release/launchxl.bin 0 bin;
  • app的,还是用launchxl例子,在tockloader里面声明烧录命令:
'launchxl-cc26x2r1': {'description': 'TI CC26x2-based launchpad',
		                      'arch': 'cortex-m4',
		                      'page_size': 512,
		                      'jlink_device': 'cc2652r1f',
		                      'jlink_speed': 4000,
		                      'jlink_if': 'jtag',
		                      'openocd': 'ti_cc26x2_launchpad.cfg',
		                      'openocd_options': ['noreset', 'resume'],
		                      'openocd_commands': {'program': 'flash write_image erase {{binary}} {address:#x};\
		                                                       verify_image {{binary}} {address:#x};'}},

tock系统的安装流程是怎样的?

  • tock编译出来elf,然后使用objcopy命令将elf转化压缩为bin文件
  • 根据主板接口不同,使用tockloader/openocd/etag等将系统bin文件烧录进地址为0x0000000的flash中

最后生成的.bin文件是怎样生成的?入口在哪里?

  • bin文件是使用LLVM-objcopy转化而来的,最后make flash的时候也是将这个.bin文件烧进主板的,实际上.bin和.elf文件是同一个文件
%.bin: %.elf
	$(Q)$(OBJCOPY) --output-target=binary $^ $@
  • 就是使用LLVM-objcopy类型GNU的OBJCOPY命令将ELF转化为bin文件

系统文件elf是怎样生成的?

  • rust默认会找到main.rs文件,打包为elf。tock的一般会配上build.rs引入内存分布说明

进程是在哪里开始执行app里面的代码的?

/// The v2 main section for apps.
///
/// All apps must have a main section. Without it, the header is considered as
/// only padding.
#[repr(C)]
#[derive(Clone, Copy, Debug)]
crate struct TbfHeaderV2Main {
    // 定义app的入口函数
    init_fn_offset: u32,
    protected_size: u32,
    minimum_ram_size: u32,
}
  • 创建进程时,调用let init_fn = app_flash_address.offset(tbf_header.get_init_function_offset() as isize) as usize;获得app的入口函数
  • 创建完进程会把init_fn连同上下文塞入process.tasks里面,多个tasks可能会指向同一个init_fn
  • 在kernel的do_process时,发现task带回调(回调就是init_fn)Task::FunctionCall(ccb),就会调用process的set_process_function,而process的set_process_function又会调用arch\cortex-m\src\syscall.rsset_process_function将app的init_fn地址(也就是callback.pc)写入PC寄存器,并且将参数写入其他寄存器。
  • 将app的代码入口写入pc寄存器后,在下一个tick就会执行该程序

process的callback是什么?

  • 就是用户编写的代码

进程的执行在哪里?

Ring buffer跟task的组合有什么作用?

app是被插入PC寄存器队列的,kernel又是死循环的,芯片是单线程的,那app是如何在kernel循环中被执行的?

  • 见下面

执行完用户app的代码,是如何切回kernel进程的?

  • systick_handler是切换的关键

内核态和用户态是如何切换的?

  • 将app的代码入口地址插入PC寄存器后,执行self.state.set(State::Running);
  • kernel循环匹配到State::Running则启动systick,通过switch_to ->switch_to_process(实际就是SP寄存器),产生systick中断,触发systick_handler中断处理函数开始执行用户app的代码

systick_handler的作用是什么?

  • systick.enable()的时候回不断产生systick中断,执行systick_handler
  • systick_handler会检查用户app代码的执行状态,如果超时或出错就会通过sp指针切换回内核进程

arch\cortex-m\src\syscall.rsswitch_to_process做了什么?

一个process有多个taskQueue,一个taskQueue又有多个task?

  • 每个IPC通信都是一个task?
  • 用户的代码是一个task?

用户进程app内申请的heap和stack是如何分配和隔离的?

  • heap的申请必须调用系统的syscall,否则mpu报错
  • stack只要指定sp指针,编译器就会自动分配

libtock的timer是什么?怎么实现的?

capsules/src/virtual_alarm.rs是什么?有什么用?

libtock-rs的DRIVER_NUMBER是每个驱动固定的值?

// driver_number见capsules\src\driver.rs
// timer
const DRIVER_NUMBER: usize = 0x00000;
// temperature
const DRIVER_NUMBER: usize = 0x60000;
// led
const DRIVER_NUMBER: usize = 0x00002;

libtock里面使用SVC 1/2/3这种来调用内核硬件,这个1/2/3如何解决并发问题?

  • 每个svc有自己不同地址callback,所以不存在并发问题

收到SVC后,在哪里开始调用硬件?

  • 调用硬件的都是系统调用syscall,对应的1/2/3/4在kernel\src\syscall.rs
  • arch\cortex-m\src\syscall.rsswitch_to_process函数有一句let syscall = kernel::syscall::arguments_to_syscall(svc_num, r0, r1, r2, r3);返回系统调用
  • 系统循环do_process检查到是Syscall::SUBSCRIBE``Syscall::COMMAND
  • 每个kernel loop循环执行前都会调用service_pending_interrupts检查nvic的地址,看有没有中断,如果有则调用对应的handle_interrupt

时钟如何倒计时的?

  • 时钟在:chips\sam4l\src\ast.rs

tock将时间处理分为时钟(alarm)和时间(time),各有什么区别?

  • alarm是定时器,只执行一次,time会执行很多次?

NVIC硬件调用逻辑

  1. libtock-rs执行汇编触发NVIC中断:
pub unsafe fn subscribe(
    major: usize,
    minor: usize,
    cb: *const unsafe extern "C" fn(usize, usize, usize, usize),
    ud: usize,
) -> isize {
    // 将结果code放到res返回
    let res;
    // // 将结果code放到res返回
    // 将major(driver_number)参数放入R0寄存器,minor(subscribe_number)放到R1寄存器
    // 回调放到R2,用户数据放到R3
    // driver_number见capsules\src\driver.rs
    llvm_asm!("svc 1" : "={r0}"(res)
                 : "{r0}"(major) "{r1}"(minor) "{r2}"(cb) "{r3}"(ud)
                 : "memory"
                 : "volatile");
    res
}
  1. tock中断执行svc_handler将R0寄存器SYSCALL_FIRED设置为1
  2. 继续内核循环do_process,通过process.switch_to();发现SYSCALL_FIRED是1,需要切换到syscall
  3. switch_to_process函数读取寄存器的参数和arguments_to_syscall函数获得syscall的类型
  4. 继续内核循环do_process根据syscall的类型比如:Syscall::SUBSCRIBE和传入的driver_num,进程id创建订阅回调,完成订阅对应硬件的capsule

NVIC订阅如何返回硬件结果?调用是在哪里?

  • 回调callback的地址和参数在触发硬件的时候塞入process.tasks里面
  • 在kernel的do_process时,发现task带回调Task::FunctionCall(ccb),就会调用process的set_process_function,而process的set_process_function又会调用arch\cortex-m\src\syscall.rs的set_process_function将callback地址写入PC寄存器,并且将参数写入其他寄存器。
  • 将callback的代码入口写入pc寄存器后,在下一个tick就会执行该程序

时钟是在哪里初始化的?如何设置的?

  • hail:
// # TIMER
    let ast = &sam4l::ast::AST;
    let mux_alarm = AlarmMuxComponent::new(ast)
        .finalize(components::alarm_mux_component_helper!(sam4l::ast::Ast));
    ast.configure(mux_alarm);
    let alarm = AlarmDriverComponent::new(board_kernel, mux_alarm)
        .finalize(components::alarm_component_helper!(sam4l::ast::Ast));
  • nrf52dk_base
let mux_alarm = components::alarm::AlarmMuxComponent::new(rtc)
        .finalize(components::alarm_mux_component_helper!(nrf52::rtc::Rtc));
    let alarm = components::alarm::AlarmDriverComponent::new(board_kernel, mux_alarm)
        .finalize(components::alarm_component_helper!(nrf52::rtc::Rtc));
  • boards\components\src\alarm.rs

cpu如何知道那个中断被订阅了?比如时钟中断,怎样使能时钟中断?

  • 系统初始化时
    • boards\nordic\nrf52dk_base\src\lib.rs初始化rtc.start();
  • app订阅驱动时
    • 使用capsules\src\alarm.rs的command函数重置alarm
    • command调用reset_active_alarm -> self.alarm.set_alarm(next_alarm);使能和设置时钟寄存器,等触发中断将调用generic_isr中断监听函数

components/capsules/chips的驱动有何区别?

  • chips是最底层的驱动,根据不同的芯片直接对接硬件,一般专门修改寄存器
  • capsule是Tock系统封装的统一的硬件驱动,是Tock对所有硬件的封装
  • components是各个board提供给用户app选择的硬件,是对capsule的封装。Tock提供非常多的capsule,但是自己的主板只能提供主板支持的capsule作为components提供给用户

COMMAND,SUBSCRIBE,ALLOW有什么区别?

  • 定义在kernel\src\syscall.rs,区别在kernel\src\sched.rs
  • subscribe是调用硬件完成后,要执行回调函数,是双向的。
  • command是单向的,仅仅是向硬件发送命令和参数,不管回调,但是可以有结果参数。所以一般是用作获取硬件的参数

如何输入调试信息

  • 可以在内核内使用:debug!("Initialization complete. Entering main loop");
  • 可以在libtock使用:
writeln!(
                console,
                "x: {}",
                x
            )?;

测试项目tests文件在哪里?

  • libtock-c/examples/tests/printf_long/main.c

像print这种在本机的terminal输出信息的是如何实现的?

  • 通过USB HID中断获取输入
  • 通过UART传输
  • 找个通过显示器主板,往里面写入显存

栈的生长方向是如何确定的?

  • https://blog.csdn.net/weixin_38233274/article/details/82713966
  • 在ARM中,一般是满堆栈,堆栈生长方向是从上向下递减的(51相反为递增),目的是为了提高内存的使用效率,不过导致出现了著名的stack overflow的错误
  • PCS即Procedure Call Standard(过程调用规范),ATPCS即ARM-THUMB procedure call standard。
  • 栈的类型有4种:
 1.FD:full descending,满递减
 2.ED:empty descending,空递减
 3.FA:full ascending,满递增
 4.EA:empty ascending,空递增

栈的大小是如何确定的?

  • app的layout都会声明各个主板的STACK_SIZE,一般是2048个指针大小(2048*32位)

data的大小是如何确定的?

  • 编译的时候编译器确定的

heap和stack之间隔着data,app_heap_start是如何确定的?默认是怎样的?

  • 在libtock的core\src\entry_point\start_item_arm.rs的汇编里,app启动的之前会先执行_start(在layout_generic.ld里面定义)这段汇编确定app_heap_start(memop(11, r4))

app的启动链接脚本是如何在哪里启动的?

  • 主要在libtock里面,先在layout_nrf52.id将layout_generic.id引入
  • build.rs里面重命名layout_nrf52.idlayout.ld
  • 最后在cargo build的时候读取.cargo\config配置里面的layout.id,链接脚本作用,每个app有自己的虚拟内存分布

grant的是做什么用的?为什么是1024?为什么放到内存顶部?

  • doc\Design.md#Grants,grant是用于跟kernel通信的,包括与驱动capsule的通信。tock不允许驱动在系统里面存数据,只允许放进程内
  • kernel\src\grant.rs,应该是没有固定大小的,就剩顶部空的,放顶部应该是最合理的选择
// Make room for grant pointers.
            // grant内存区域的指针
            // 返回指针的大小
            let grant_ptr_size = mem::size_of::<*const usize>();
            let grant_ptrs_num = kernel.get_grant_count_and_finalize();
            // 下一个指针的地址
            let grant_ptrs_offset = grant_ptrs_num * grant_ptr_size;


let ctr_ptr = process.grant_ptr(self.grant_num) as *mut *mut T;
                    // If the pointer at that location is NULL then the grant
                    // memory needs to be allocated.
                    let new_grant = if (*ctr_ptr).is_null() {
                        process
                            .alloc(size_of::<T>(), align_of::<T>())
                            .map(|root_arr| {
                                let root_ptr = root_arr.as_mut_ptr() as *mut T;
                                // Initialize the grant contents using ptr::write, to
                                // ensure that we don't try to drop the contents of
                                // uninitialized memory when T implements Drop.
                                write(root_ptr, Default::default());
                                // Record the location in the grant pointer.
                                write_volatile(ctr_ptr, root_ptr);
                                root_ptr
                            })
                    } else {
                        Some(*ctr_ptr)
                    };

app rust代码里面申请的堆和栈是如何映射到系统的内存分配的?

  • rust内部只使用栈分配
  • rust只使用栈,堆需要用户自己申请,而自己申请只能调用tock系统的syscall(4)来申请堆

虚拟内存跟进程有何关系?sp指针指向的虚拟内存还是真实内存?

sp指针代表栈指针?

  • 是的

mpu是如何判断野指针访问内存的?

  • https://blog.csdn.net/u010961173/article/details/102380900
  • 操作系统可以为这些域分配更多的属性:访问权限、cache和写缓存。存储器基于当时的处理器模式(管理模式或用户模式)可以设定这些区域的访问权限为读/写、只读和不可访问。
  • 当处理器访问主存的一个域时,MPU比较该域的访问权限属性和当时的处理器模式。如果请求符合域的访问标准,则MPU允许内核读/写主存;如果存储器请求不符号域的访问标准,将产生一个异常信号。

访问权限、cache和写缓存分别是什么意思?

  • https://blog.csdn.net/ldld1717/article/details/50557129
    到2000年,DRAM部件每片的容量到达256Mbit,随机访问速率在30MHz左右。微处理器每秒需要访问存储器几百兆次。如果处理器速率远高于存储器,那么只能借助Cache才能满足其全部性能。
    Cache存储器是一个容量小但存取速度非常快的存储器,它保存最近用到的存储器数据拷贝。对于程序员来说,Cache是透明的。它自动决定保存哪些数据、覆盖哪些数据。现在Cache通常与处理器在同一芯片上实现。Cache能够发挥作用是因为程序具有局部性特性。所谓局部性就是指,在任何特定的时间,微处理器趋于对相同区域的数据(如堆栈)多次执行相同的指令(如循环)。
    Cache经常与写缓存器(write buffer)一起使用。写缓存器是一个非常小的先进先出(FIFO)存储器,位于处理器核与主存之间。使用写缓存的目的是,将处理器核和Cache从较慢的主存写操作中解脱出来。当CPU向主存储器做写入操作时,它先将数据写入到写缓存区中,由于写缓存器的速度很高,这种写入操作的速度也将很高。写缓存区在CPU空闲时,以较低的速度将数据写入到主存储器中相应的位置。
    通过引入Cache和写缓存区,存储系统的性能得到了很大的提高,但同时也带来了一些问题。比如,由于数据将存在于系统中的不同的物理位置,可能造成数据的不一致性;由于写缓存区的优化作用,可能有些写操作的执行顺序不是用户期望的顺序,从而造成操作错误。

tock没有MMU,有实现4G虚拟内存和内存映射到物理内存吗?

  • tock不支持虚拟内存,因此才需要每个app都编写layout_generic.ld声明内存布局
  • tock同时不支持多进程,IPC通信也只是为了方便进程驱动和进程交换数据

不支持虚拟内存,rust创建变量的时候是如何知道目标栈地址?sp指针?

  • 创建app的时候还要编写.cargo\config的rustflags,使用c的link-arg传递参数给连接器,用-Tlayout.ld指定连接脚本声明内存布局
  • ARM处理器针对不同的模式,共有 6 个堆栈指针(SP),其中用户模式和系统模式共用一个SP,每种异常模式都有各自专用的R13寄存器(SP)。它们通常指向各模式所对应的专用堆栈,也就是ARM处理器允许用户程序有六个不同的堆栈空间。这些堆栈指针分别为R13、R13_svc、R13_abt、R13_und、R13_irq、R13_fiq
  • 是的,app的栈其实就用户模式的sp指针,就是R13,因此只要在汇编种设置好sp指针的地址,编译器就会顺着设置变量
  • 因为堆是需要用户手动通过系统模式的系统调用申请的,否则会导致mpu报错

包括寄存器的所有信息nRF52832_PS_v1.4.pdf

@cisen
Copy link
Owner Author

cisen commented Feb 9, 2020

kernel

包括功能

  • ipc通信
  • 内存管理
  • 进程调度(schedule)线程管理(process)
  • 驱动管理
  • debug
  • 硬件管理(hil)
    • crc
    • flash
    • gpio
    • led
    • usb
    • uart

systick

  • 一个可以选择时钟源为HLI/HLE振荡器的不断倒计时的时钟
  • 可以产生中断

@cisen
Copy link
Owner Author

cisen commented Mar 15, 2020

tockloader

问答

  • ti的板子问题:cc26x2r1_launchxl build passed, but can't flash zephyrproject-rtos/zephyr#21372
  • tockloader install --board launchxl-cc26x2r1 --openocd blink即可烧入app
    • tockloader install --jlink --arch cortex-m4 --board nrf52dk --jtag-device nrf52 /home/cisen/develop/tock/libtock-rs/target/thumbv7em-none-eabi/tab/nrf52/hello_world.tab
    • tockloader uninstall --jlink --arch cortex-m4 --board nrf52dk --jtag-device nrf52
    • make flash-nrf52 EXAMPLE=hello_world
  • tockloader info --jlink --board nrf52dk
  • make nrf52 DEBUG=info
  • make flash-nrf52 EXAMPLE=button_leds DEBUG=debug/info/warn/error/fatal
  • JLinkExe -device nrf52 -if swd -speed 1000 -autoconnect 1
  • JLinkRTTClient
  • 成功再libtock-c烧进去:tockloader install --board nrf52dk --jlink blink/build/blink.tab

tockloader是如何将bin文件写进硬件的?

程序进程的数据结构:

  • BSS段(RAM):BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。由操作系统分配,全局的,进程申请由编译器决定
  • .data数据段(RAM):数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。由操作系统分配,全局的,进程申请由编译器决定
  • .text代码段(ROM):代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
  • 堆(heap)(RAM):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。由进程决定,局部的,用户申请
  • 栈(stack)(RAM):栈又称堆栈,用户存放程序临时创建的局部变量。在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的后进先出特点,所以栈特别方便用来保存/恢复调用现场。由进程决定,局部的,用户申请

全局静态区,文字常量区,程序代码区是从内存地址分配的角度来描述的。

  • 全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。

  • 文字常量区—常量字符串就是放在这里的。

  • 程序代码区—存放函数体的二进制代码。

@cisen
Copy link
Owner Author

cisen commented Mar 22, 2020

调试

# mac 
brew tap ARMmbed/homebrew-formulae && brew update && brew install arm-none-eabi-gcc
$ cd tock/boards/nrf52dk/jtag
# 重要侦听2331端口
$ ./jdbserver_pca10040.sh
$ JLinkGDBServer -device nrf52 -speed 1200 -if swd -AutoConnect 1 -port 2331
arm-none-eabi-gdb -x gdbinit_pca10040.jlink
# debian
sudo apt install gdb-arm-none-eabi gdb-multiarch
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "nRF52-DK",
            "type": "gdb",
            "request": "attach",
            "executable": "${workspaceRoot}/boards/nordic/nrf52dk/target/thumbv7em-none-eabi/release/nrf52dk",
            "target": "localhost:2331",
            "remote": true,
            "cwd": "${workspaceRoot}",
            "gdbpath": "gdb-multiarch",
            "autorun": [
                "monitor reset",
                "monitor speed auto",
                "b reset_handler"
            ]
        }
    ]
}

@cisen
Copy link
Owner Author

cisen commented Jun 14, 2020

改bug

跟踪

总结

  • capture任务,手动触发CAPTRURE[n]任务,Timer的值就会被复制到CC[n]寄存器,一般用于获取当前Timer的值,不会产生中断
  • compare事件,Timer的值增长到等于CC[n]就自动会生成COMPARE[n]事件和中断(可以设置),中断的时候可以通过获取COMPARE[n]知道是哪个事件点的值
  • 毫秒转化多少ticks,直接将毫秒转化未秒然后乘以频率就好
  • prev一定是最小的,要不不会再set_alarm更新prev,所以可以用prev作为参考
  • set_alarm通常是多少毫秒之后,肯定是正值,也就是when肯定是大于now的
  • 关键是处理alarm2中断的时候,由于设置了很多很多alarm,fired那里数组很大而且还要对alarm进行最小排序,alarm1也到期了,这时候alarm1的中断就有可能被错过
  • 是因为set_alarm时没办法保证when大于now?如果设置的when值小于now,那么when只能在下一个循环再触发中断了,when是当前设置了中断的时钟,每次用户set_alarm或者fired调set_alarm都会更新when,如果两者频率非常高,而且when是未来值,就会出现when比now小
> in VirtualMuxAlarm::set_alarm
alarms:       | alarm 1   |      | alarm 2
before:  prev | cur_alarm | now  | when(设置1)
after:        |           | prev | cur_alarm(设置2)

# 这样才对?
alarms:                  | alarm 2     |          | alarm 1     |
before:  prev/now | when        |          | cur_alarm  |
after:     prev/now(fake)| cur_alarm |          |     | now(acturelly)

alarm=16779325, now=2109, prev=16776056, nowWrapPrew=4278193349, alarmWrapPrev=3269
// has_expired函数
fn has_expired(alarm: u32, now: u32, prev: u32) -> bool {
    now.wrapping_sub(prev) >= alarm.wrapping_sub(prev)
    if (now as i32).wrapping_sub(prev as i32) < 0
        || (alarm as i32).wrapping_sub(prev as i32) < 0
    {
        panic!("now={}, alarm={}, prev={}", now, alarm, prev);
    }
}
  • 原因流程
    • 已设置alarm1,alarm1中断未被触发
    • 正在设置alarm2,导致prev被修改,alarm1未被触发
    • alarm1中断触发,由于prev被修改了,导致中断遍历到alarm1再判断超时时失败,alarm1要在下一个Timer循环种才能被中断检测到
    • 也就是设置alarm2时,刚好在alarm1时刻和触发alarm1中断的时间之间
    • alarm/now/prev都是无符号整型,alarm-prev大于now-prev时会出现的:
      • 设置alarm1跟alarm2的间距很小,设置alarm2时,alarm1到期了还没执行,fired的时候就会那alarm2的prev去跟now和alarm1比较。这时候就会出现alarm1-prev>now-prev。在Segger RTT功能种特别明显,因为RTT设置的alarm1跟alarm2之间的时间间隔未100us
# now - prev = 3 < alarm1 - prev = 4导致alarm1要在下一个tick循环才能被捕获到
--alarm1----prev-alarm2--alarm1中断(now)----------

      • 如果alarm1跟alarm2间距很大,出现设置alarm2的时候alarm1到期了还没执行的概率就很小
      • 如果时钟中断随机设置,也很有可能出现alarm1跟alarm2很接近的情况,就会出现alarm1要等很久才能被捕获

问题翻译

这个计时器的问题跟这些最新的issue的是一样的。
前提
我开始注意到这个问题是整合RTT的时候,通过syscall的console打印了一堆信息。app有些时候会被冻结,似乎在永远等待console驱动程序返回“写完成”回调。因为debug的输出也会被冻结,我不是百分百肯定。但是通过LED的闪来调试确实发现这个问题。

在virtual_alarm capsule中,有很多针对某个参考的相互比较时间戳的检查(比如“now”)

第一个检查,如果新的时钟设置的到期时间戳when在当前已设置时钟的到期时间戳之前(这种情况时钟应该更新到期时间戳到when)
tock/capsules/src/virtual_alarm.rs
Lines 85 to 91 in d52a619

 let cur_alarm = self.mux.alarm.get_alarm(); 
 let now = self.now(); 
  
 if cur_alarm.wrapping_sub(now) > when.wrapping_sub(now) { 
     self.mux.prev.set(self.mux.alarm.now()); 
     self.mux.alarm.set_alarm(when); 
 } 

第二个检查,如果一个alarm跟now相比已经到期了(alarm是设定值,now是时钟动态的当前值,prev是前一个设定值,when是下一个设定值)
tock/capsules/src/virtual_alarm.rs
Lines 131 to 133 in d52a619

 fn has_expired(alarm: u32, now: u32, prev: u32) -> bool { 
     now.wrapping_sub(prev) >= alarm.wrapping_sub(prev) 
 } 

我不确定wrapping_sub算法的逻辑含义是什么,但是这似乎在保证 cur_alarm> = now && when> = now(在第一种情况下),以及now> = prev && alarm> = prev(在第二种情况)。
否则,由于时间戳是无符号整数,因此如果其中一个参数稍微小于now(在第一种情况下)或prev(在第二种情况下),则wrapping_sub算法将产生非常大的数字。

然而,这些假设在现在的代码种是不会出现的,不管通过插入一些紧急语句却很容易生成示例。

第一种情况,下面的代码很容易让我的app出现panic

if (cur_alarm as i32).wrapping_sub(now as i32) < 0
    || (when as i32).wrapping_sub(now as i32) < 0
{
    panic!("cur_alarm={}, when={}, now={}", cur_alarm, when, now);
}
Kernel panic at [...]/tock/capsules/src/virtual_alarm.rs:91:
	"cur_alarm=10351, when=10356, now=10353"

第二种情况发生panics的频率要少得多,但是我仍然可以在某个时候触发它(您可以看到systicks的数字要大得多)。

if (now as i32).wrapping_sub(prev as i32) < 0
    || (alarm as i32).wrapping_sub(prev as i32) < 0
{
    panic!("now={}, alarm={}, prev={}", now, alarm, prev);
}
Kernel panic at [...]/tock/capsules/src/virtual_alarm.rs:141:
	"now=7392136, alarm=7392134, prev=7392135"

在我观察到的两种panic中,参考点都在其他两者之间,从而使比较“翻转”。

例如,在第二种情况下,alarm = 7392134 <prev = 7392135 <now = 7392136,但是代码却得到尚未到期的结论(这似乎是错误的)。

请注意,alarm capsule中有相同的功能。
tock/capsules/src/alarm.rs
Lines 161 to 163 in d52a619

 fn has_expired(alarm: u32, now: u32, prev: u32) -> bool { 
     now.wrapping_sub(prev) >= alarm.wrapping_sub(prev) 
 } 

无论如何,我不确定逻辑到底应该是什么。 看来在i32格式上进行比较会更准确。 virtual_alarm的多路复用器还可以更好地跟踪过去是否触发了哪些alarm或“只是发生了”(即发生在过去的2 ^ 31个ticks中?),这与#1496和#1499有关。 或确保上一个alarm确实早于任何其他alarm(与第二次panic不同)。

我还想知道“参考点”是否在此比较中带来了任何价值,而不是在i32上进行wrapping_sub,如下所示:

fn has_expired(alarm: u32, now: u32) -> bool {
    (now as i32).wrapping_sub(alarm as i32) >= 0
}

最终,ticks会回绕,但是在这些比较中,我认为应该将时间戳假定为“足够接近”(这样i32会更准确)。 当然,如果您按2 ^ 31个ticks的顺序等待,那么所有关于比较逻辑正确性的押注都会被取消。

我还认为,更好的输入数据结构会有所帮助,例如,使用“ Timestamp”和“ Duration”类型代替无处不在的u32类型。

无论如何,似乎在这里非常需要更多的单元测试(即#1512)来模拟alarm muxer 在各种情况下的行为。

关于此问题的一些更新:通过运行libtock-rs中的blink_random示例,可以在virtual_alarm.rs中的has_expired中产生以下情况。

Kernel panic at [...]/tock/capsules/src/virtual_alarm.rs:141:
	"now=1291, alarm=16778507, prev=16775234"

当转换为十六进制时,now= 0x50b,alarm= 0x100050b,prev= 0xfff842。 因此,似乎systick只有24位(cc#1413),并且alarm实际在逻辑上应等于now

在那种情况下,我们得到now.wrapping_sub(prev)= 0xff000cc9alarm.wrapping_sub(prev)= 0xcc9,因此has_expired函数能正确地返回alarm已过期,尽管它会错误地得出now值: now= 1290

1496 修复virtual_uart 种的transmit_buffer 函数在异步回调种的bug

tock/capsules/src/virtual_uart.rs
Line 353 in ad676ee

 self.mux.do_next_op(); 

有传输请求时调用do_op。 如果没有待处理的请求,则可以立即处理。 如果失败,则发出带有错误的callback_done回调:
tock/capsules/src/virtual_uart.rs
Line 190 in ba11fd9

 client.transmitted_buffer(rbuf.unwrap(), 0, rcode); 

我们已经同意,出于竞争条件/数据完整性的原因,不应在调用期间发生回调。 如果立即处理了对虚拟UART的发送调用且失败了,则将同步触发回调。 相反,这应该使用延迟调用来异步发出回调。

1499 添加alarm的测试

此拉取请求增加了对schedule timer的支持,其中提供的systick counter实际上位于过去。 这对于将timer设置为很短的时间很有用。 如果在一个应用程序中同时调度计时器,就会发生这种情况,想象两个LED同时以500和250ms的间隔闪烁。 一个延迟将在第二个停止后开始,并且会任意缩短。
这很容易导致(不可避免的)情况,即设置为alarm的systick count其实已经结束。

此PR通过firing alarm(如果已结束)来解决此问题。

1651 VirtualMuxAlarm 错误

使用VirtualMuxAlarm设置alarm时,参考点(self.mux.prev)与alarm一起设置。
tock/capsules/src/virtual_alarm.rs
Lines 84 to 95 in f30961f

 if enabled > 0 { 
     let cur_alarm = self.mux.alarm.get_alarm(); 
     let now = self.now(); 
  
     if cur_alarm.wrapping_sub(now) > when.wrapping_sub(now) { 
         self.mux.prev.set(self.mux.alarm.now()); 
         self.mux.alarm.set_alarm(when); 
     } 
 } else { 
     self.mux.prev.set(self.mux.alarm.now()); 
     self.mux.alarm.set_alarm(when); 
 } 

然后,该上一个参考点用于确定多路复用的alarms中的alarm是否已过期。
tock/capsules/src/virtual_alarm.rs
Line 149 in f30961f

 .filter(|cur| cur.armed.get() && has_expired(cur.when.get(), now, prev)) 

问题
问题在于,在VirtualMuxAlarm :: set_alarm内部无法保证cur_alarm> = now。 我们可能会处在:已经设置了alarm1,正在设置alarm2,而alarm1已经在过去(但尚未触发)的情况。

> in VirtualMuxAlarm::set_alarm
alarms:       | alarm 1   |      | alarm 2
before:  prev | cur_alarm | now  | when(设置1)
after:        |           | prev | cur_alarm(设置2)

在这种情况下,由于新的prev就在alarm1之后,因此下次调用MuxAlarm :: fired时,alarm1将不会被视为过期-而是需要花费ticks轮回的时间才能判断时钟已经到期。

这在Nordic上的Segger RTT调试中尤其明显,该调试器将计时器设置为将来非常接近(100us)。 到其他代码运行时,这可能已经过去了。
tock/capsules/src/segger_rtt.rs
Line 243 in f30961f

 let interval = (100 as u32) * <A::Frequency>::frequency() / 1000000; 

但是使用virtual alarms的所有capsule都会受到影响。

可观察到的结果是alarm客户端(在本例中为Segger RTT)永远等待(对于Segger RTT,内核调试和控制台似乎冻结)。

解决方案
我认为在设置新alarm时移动prev是不正确的。 仅应在MuxAlarm :: fired函数中对其进行更新,在该函数中实际上会触发所有过期的alarm。
这再次表明32位时间戳的“三向比较”容易出错,因为实现起来很复杂,难以理解,很难知道何时可以安全地更新参考点,等等。我认为这是 关于在与时间相关的胶囊和HIL中具有真正单调(即64位)时间戳的另一个论点(如先前在https://groups.google.com/d/msg/tock-dev/CWecIZq7jqg/RrWySQLbBgAJ中讨论的)。

delay_ms (1) blocks the process (and maybe the whole alarm system) on stm boards

问答

rtc时钟是如何设置时间的?

alarm的CAPTURE/COMPARE 模式是什么?

  • DCD TIM1_CC_IRQHandler ; TIM1 Capture Compare
  • DCD TIM8_CC_IRQHandler ; TIM8 Capture Compare
  • 关于TIM的操作,要注意的是STM32处理器因为低功耗的需要,各模块需要分别独立开启时钟,所以,一定不要忘记给用到的模块和管脚使能时钟
  • capture就是倒计时到0然后触发中断(溢出),compare是设置一个值,每次震动都比较一次,匹配就产生中断?
  • 每次手动触发CAPTURE[n]任务时,计数器值都会被复制到CC [n]寄存器中。
  • 当计数器递增,然后等于capture compare寄存器之一中指定的值时,将生成一个COMPARE事件。 当计数器值等于capture compare寄存器CC[n]中指定的值时,将生成相应的比较事件COMPARE [n]。总结就是计数器的值等于CC[n]就自动会生成COMPARE[n]事件

VirtualMuxAlarm是什么?

  • 虚拟多路复用时钟,就是存储了很多时钟的一种结构体,每次fired都会检查所有时钟的到期时间并执行所有到期时钟
  • 主要给蓝牙,网络等内部协议使用

self.now和alarm.now有何区别?

cur_alarm,now,when,prev分别是什么?

  • cur_alarm是当前cc寄存器当前的倒计值
  • now是当前设置倒计中断的值
  • when是下一个设置倒计中断的值
  • prev是前一个倒计中断的值

Timer和Count两种模式有何区别?

  • 都可以通过设置START/STOP task启动或者恢复
  • Timer时从0开始,每个tick Timer的计数寄存器就+1
  • Timer计数寄存器的最大数值由BITMODE寄存器设置,可以时8位/16位/24位/32位
  • 当Timer的计数寄存器达到了最大值,再+1的时候会自动从0重新开始计数
  • 可以通过CLEAR task清除Timer的计算器寄存器的值
  • Counter模式下,每次被触发就会往Timer的计数寄存器+1
  • Counter模式在Timer模式时无效
  • 一定要先停止Timer计数寄存器在设置其他,否则会有不可预料的结果

既然MuxAlarm.virtual_alarms[0]=VirtualMuxAlarm,就是MuxAlarm是VirtualMuxAlarm的父,那VirtualMuxAlarm是如何被push进去virtual_alarms的?

  • 不能通过MuxAlarm插入VirtualMuxAlarm,只能通过VirtualMuxAlarm.set_client方法插入VirtualMuxAlarm
  • 也就是一般是直接操作VirtualMuxAlarm来控制父的,父MuxAlarm只能fired的时候遍历virtual_alarms并调用子的fired

系统的所有时钟都是在MuxAlarm下进行管理的?

  • 是的,在boards\components\src\alarm.rs有个AlarmMuxComponent组件给各个主板使用rtc初始化的时候创建一个公共的MuxAlarm
  • 然后就可以创建很多个AlarmDriverComponent(通过AlarmDriver::new)client来订阅统一的时钟中断

AlarmDriver跟VirtualMuxAlarm有什么关系?有什么区别?

  • AlarmDriver.alarm=VirtualMuxAlarm
  • VirtualMuxAlarm.client=AlarmDriver
  • AlarmDriver是专门对接app的对象,有subscribe, command
  • VirtualMuxAlarm是对接系统全局的MuxAlarm,是用于被系统调度的。所以VirtualMuxAlarm是时钟内核,既对接app的AlarmDriver,又对接操作系统的MuxAlarm
  • VirtualMuxAlarm负责遍历所有的alarm和调用原生的alarm
  • AlarmDriver负责保持用户的回调和执行用户的订阅
  • MuxAlarm 负责维护VirtualMuxAlarm链表和真实alarm
  • 一个VirtualMuxAlarm对应一个alarmclient
  • MuxAlarm就只是被VirtualMuxAlam使用的,因此不会存kernel

每次订阅一次就创建一个virtualAlarm?

  • 一个驱动有一个VirtualMuxAlarm维护alarm跟系统的关系,一般也就一个驱动一个VirtualMuxAlarm
  • 一个app可以订阅很多个驱动,因此会在Grant存一份各个驱动订阅的时间列表
  • 订阅alarm跟VirtualMuxAlarm的fired是完全独立,VirtualMuxAlarm有个mux链表维护,订阅有个grant内存存储client订阅的时间
  • 因为只支持单线程,因此只支持暂停当前线程。也就是说sleep的时候全部卡住。也就是说一个线程只能有一个alarmclient
  • 一个进程订阅多个驱动的时钟是通过fired的时候遍历app_alarm(驱动和内核共享的区域,链表,用于存储订阅 的时间)

app_alarm是什么?

  • 内核跟驱动共享的区域,是一个链表

一个app只能setTimeout一次?

  • 是的,内核只支持一个,如果sleep了,由于是单线程,因此会一直停顿住
  • 也是由于这个,一个用户进程只能调用alarm驱动一次

可以通过创建多个timeDriver实现多个setTimeout?

  • 不能
  • 要实现多个setTimeout是依赖future的poll实现的,实际上还是通过封装poll轮询一个setTimeout,这个setTimeout到期(根据tick确定)后在插入新的setTimeout。实现在src\timer.rs的函数activate_current_timer

寄存器

# 几个重要的缩写的意义:

CC: Capture compare

CCXE: Capture/Compare x output enable

CCXNE:Capture/Compare 1 complementary output enable

CCXNP:Capture/Compare x complementary output polarity

OPM:The one-pulse mode (OPM)

TI: Timer 

TIx: x Timer number,TIx Timer X,the timer channel inputs, TIx inputs.

IT: internal Trigger 

ITx:internal Trigger x

TI1FP1:Timer Input 1 Filtered Priority channel 1

ED: Edge Detector:

FDTS: Frequency of Division Timer Clock:The FDTS clock signal is derived from the timer clock signal, and the CKD[1:0] control bit-field sets the ratio between these two clock signals.

fCK_INT: Frequency clock of internal

fDTS = FDTS

TIxF_ED:TIx Edge Detector

ETRF:External Trigger input

Internal Trigger 0 (ITR0).

Internal Trigger 1 (ITR1).

Internal Trigger 2 (ITR2).

Internal Trigger 3 (ITR3).

TI1 Edge Detector (TI1F_ED)

Filtered Timer Input 1 (TI1FP1)

Filtered Timer Input 2 (TI2FP2)

External Trigger input (ETRF)

trigger signal (TRGI)
  • 16777216 = 2^24B÷bai1024÷1024=16MB
  • chips\nrf52\src\adc.rs写着:/// Capture and compare value. Sample rate is 16 MHz/CC
  • 如果您使用nRF52840的RTC,则对于32KHz的频率,溢出频率为2 ^ 24,因此为512秒,即仅超过8分钟。
# capsules/src/virtual_alarm.rs
alarm=16779325, now=2109, prev=16776056, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779355, now=2139, prev=16776086, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779392, now=2176, prev=16776123, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779425, now=2209, prev=16776156, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779463, now=2247, prev=16776194, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779356, now=2140, prev=16776087, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779415, now=2199, prev=16776146, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779430, now=2214, prev=16776161, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779469, now=2253, prev=16776200, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779505, now=2289, prev=16776236, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779409, now=2193, prev=16776140, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779444, now=2228, prev=16776175, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779453, now=2237, prev=16776184, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779475, now=2259, prev=16776206, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779542, now=2326, prev=16776273, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779460, now=2244, prev=16776191, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779485, now=2269, prev=16776216, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779515, now=2299, prev=16776246, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779605, now=2389, prev=16776336, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779646, now=2430, prev=16776377, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779576, now=2360, prev=16776307, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779618, now=2402, prev=16776349, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779682, now=2466, prev=16776413, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779745, now=2529, prev=16776476, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779810, now=2594, prev=16776541, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779759, now=2543, prev=16776490, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779778, now=2562, prev=16776509, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779831, now=2615, prev=16776562, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779868, now=2652, prev=16776599, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779885, now=2669, prev=16776616, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779810, now=2594, prev=16776541, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779826, now=2610, prev=16776557, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779914, now=2698, prev=16776645, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779985, now=2769, prev=16776716, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779999, now=2783, prev=16776730, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779932, now=2716, prev=16776663, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16779997, now=2781, prev=16776728, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780031, now=2815, prev=16776762, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780095, now=2879, prev=16776826, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780141, now=2925, prev=16776872, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780020, now=2804, prev=16776751, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780031, now=2815, prev=16776762, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780073, now=2857, prev=16776804, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780127, now=2911, prev=16776858, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780142, now=2926, prev=16776873, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780091, now=2875, prev=16776822, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780162, now=2946, prev=16776893, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780279, now=3063, prev=16777010, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780325, now=3109, prev=16777056, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780358, now=3142, prev=16777089, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780365, now=3149, prev=16777096, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780254, now=3038, prev=16776985, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780288, now=3072, prev=16777019, nowWrapPrew=4278193349, alarmWrapPrev=3269
alarm=16780307, now=3091, prev=16777038, nowWrapPrew=4278193349, alarmWrapPrev=3269

@cisen
Copy link
Owner Author

cisen commented Jun 15, 2020

进程总结

进程变量

问答

tbf中,创建进程的核心参数是什么?

  • app文件的大小:header.base.total_size
  • app运行的最小RAM: header.main.minimum_ram_size
  • app的名字:header.package_name
  • app指令入口位置:hd.base.header_size,也就是header之后就直接是代码了
  • 当前系统指针的大小(栈大小)
  • 当前ram顶部指针的位置,用于计算预留给app进程的内存空间,包括进程跟内核通信的ram和minimum_ram_size。创建一个进程,需要给内核一段内存和给进程一段内存

@cisen cisen added the Tock label Jun 23, 2020
@cisen
Copy link
Owner Author

cisen commented Sep 12, 2020

URAT 相关

问答

各个芯片不同的针脚定义在哪里?

  • 在各个board里面定义阵脚,比如boards\nordic\nrf52dk\src\main.rs
  • 在各个chip里面实现完整功能的UART: chips\nrf52\src\uart.rs

寄存器有什么用?

如何发送一大波数据?如何分批发送

  • capsule的console仅支持发送U8的buffer,buffer的划分tock系统是没有做的,要做的话也是应用程序自己做比如:
    src\console.rs的:
impl Console {
    pub fn write<S: AsRef<[u8]>>(&mut self, text: S) -> TockResult<()> {
        let mut not_written_yet = text.as_ref();
        while !not_written_yet.is_empty() {
            let num_bytes_to_print = self.allow_buffer.len().min(not_written_yet.len());
            self.allow_buffer[..num_bytes_to_print]
                .copy_from_slice(&not_written_yet[..num_bytes_to_print]);
            self.flush(num_bytes_to_print)?;
            not_written_yet = &not_written_yet[num_bytes_to_print..];
        }
        Ok(())
    }
}

发送流程是怎样的?

  • 见libtock的src/console,先将buffer分块,然后再将buff入口指针设置到寄存器里面,最后调用发送
fn flush(&mut self, num_bytes_to_print: usize) -> TockResult<()> {
        let shared_memory = syscalls::allow(
            DRIVER_NUMBER,
            allow_nr::SHARE_BUFFER,
            &mut self.allow_buffer[..num_bytes_to_print],
        )?;

        let is_written = Cell::new(false);
        let mut is_written_alarm = || is_written.set(true);
        // 设置发送完的回调函数
        let subscription = syscalls::subscribe::<Identity0Consumer, _>(
            DRIVER_NUMBER,
            subscribe_nr::SET_ALARM,
            &mut is_written_alarm,
        )?;
        // 触发发送函数
        syscalls::command(DRIVER_NUMBER, command_nr::WRITE, num_bytes_to_print, 0)?;

        unsafe { executor::block_on(futures::wait_until(|| is_written.get())) };

        mem::drop(subscription);
        mem::drop(shared_memory);

        Ok(())
    }

write_buffer和tx_buffer有什么关系?

  • write_buffer是应用的将要发送的数据,tx_buffer是console驱动要发送的东西
fn send_continue(&self, app_id: AppId, app: &mut App) -> Result<bool, ReturnCode> {
        if app.write_remaining > 0 {
            app.write_buffer
                .take()
                .map_or(Err(ReturnCode::ERESERVE), |slice| {
                    // 这里slice就会转化为tx_buffer
                    self.send(app_id, app, slice);
                    Ok(true)
                })
        } else {
            Ok(false)
        }
    }

console是如何初始化的?

  • boards\nordic\nrf52dk_base\src\lib.rs搜索ConsoleComponent

release

@cisen
Copy link
Owner Author

cisen commented Sep 22, 2020

kernel 机会查找

关键文件

  • kernel\src\debug.rs
  • kernel\src\ipc.rs
  • kernel\src\process.rs
  • kernel\src\sched.rs

@cisen
Copy link
Owner Author

cisen commented Mar 8, 2022

安装到swerv

  • [*] 编译swerv到fpga

  • 问题列表

    • swerv没有匹配ddr。自己用swerv+swervold,使用make方式,发现include路径错误和各种core匹配错误,放弃自己配
    • 找到另外一个带ddr的,编译报错,注意include和verilog可以使用systemverilog类型运行
  • 运行汇编点亮led

  • 安装系统到swerv

问答

需要准备什么?

需要sw暴露什么接口?只要内存分布就够了?

如何编译riscv版本?

cd boards/hail/
make

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant