开发环境:SEGGER Embedded Studio V5.32a
NCS版本:v1.5.1
开发板:PCA10095
项目路径:
开发板路径:
项目装载:
注意上图的 nRF Connect SDK Release 和 nRF Connect Toolchain Version 也可以选择:
但是前提是在SES中配置好:
注意: 路径按照自己安装的NCS选择。
编译完成:左边是build,右边是debug
烧入现象:LED1周期性闪烁。
代码解析:
1.宏定义:
LED0_NODE:
由以上两个宏定义可得 LED0_NODE 等价于 DT_N_ALIAS_led0
DT_NODE_HAS_STATUS:
这个用来判断设备是否OK的宏比较复杂,此处使用#if,可以看出其实就是判断此宏定义是为0还是为1。我们深入里面查看,首先替换掉第一层得到:
替换掉第二层:
替换掉第三层:
第四层替换很巧妙:
首先这里我们需要拼接 _XXXX 和 config_macro 但是注意,此时的config_macro 实际内容是:
LED0_NODE_STATUS_okay
替换掉上个宏定义得到的内容实际是:
DT_N_ALIAS_led0_STATUS_okay
这个宏是一个由脚本自动根据设备树文件生成的宏,它在 devicetree_unfixed.h 中被定义:
所以,如果在设备树中,led0的状态为okay,则此宏自动被定义且为1!所以此处替换结果为:
注意最重要的一点,下面有 _XXXX1 的定义:
这个逗号极为重要。 我一开始忽略了逗号就导致代码看不明白。
这里分为两种情况:
1. DT_N_S_leds_S_led_0_STATUS_okay 宏没有被定义,则此处替换为:
Z_IS_ENABLED2(_XXXX)
2.DT_N_S_leds_S_led_0_STATUS_okay 宏被定义了,且为1,则此处替换为:
Z_IS_ENABLED2(_XXXX1)
进一步被替换为:
Z_IS_ENABLED2(_YYYY,)
替换第5层和第六层:
1.如果上一步结果是 Z_IS_ENABLED2(_XXXX) ,则替换结果为:
很明显最后结果为0。
2.如果上一步结果是 Z_IS_ENABLED2(_XXXX1) ,则替换结果为:
很明显结果为1。其实就是因为上面那个逗号。所以追溯到最后,此宏会判断在其他.h里是否定义了某个宏,来确定返回值是否为1。
LED0:
详细过程不再赘述,替换结果为:DT_N_S_soc_S_peripheral_50000000_S_gpio_842500_P_label
即 "GPIO_0"
PIN:
详细过程不再赘述,替换结果为:
DT_N_S_leds_S_led_0_P_gpios_IDX_0_VAL_pin
即 28
FLAGS:
详细过程不再赘述,替换结果为:
DT_N_S_leds_S_led_0_P_gpios_IDX_0_VAL_flags
即 1
至此,所有的宏定义都已经被分析完。
2.主体代码
可以看到主要代码分为3步:
1.通过LED0获取设备相关信息
2.根据设备信息中的引脚号去初始化相关引脚
3.在while循环里闪烁led
device_get_binding()
可以看到这个函数使用inline关键字修饰,代表其为内联函数,也就是在实代码编译过程中,函数名会被替换为函数体内容,所以C语言中内联函数一般被定义在.h文件中。好处就是用空间换时间。
compiler_barrier()为编译器内存屏障,防止编译器优化指令执行顺序,在驱动中较为常见,因为驱动设备一般来说需要先初始化才能使用,但是有时候编译器优化会导致指令乱序执行。
另外一个函数根据名称获取设备的函数,这个过程被认为是绑定。根据上一节宏定义,我们已经知道这个函数的入参其实就是 LED0 也就是字符串 "GPIO_0" ,此函数通过两个循环,去轮询__device_start 到 __device_end 之间的所有dev,然后比对每个dev的名字指针,或名字字符串是否相同去判断是否是名字为入参的dev,且此设备必须为 ready 状态。
ready状态怎么判断?
在zephyr中,把所有设备按顺序排列后,每个设备用1bit来表示状态。比如我把所有设备状态存在地址为0x00000000的内存里,我们知道在32位单片机里,物理地址是unsigned long 类型,所以每32个设备我们存在一个地址里面,然后累加存下面32个设备状态。从代码里面印证我们的分析:
这里是一个简单的计算,比如我们找的设备排在第35个,首先我们35/32 = 1余3,所以 sys_test_bit()的两个参数分别为:__device_init_status_start + 1 和 3 ,也就是第35个设备的状态储存在,以32个设备为一组划分,第二个组里面bit3。注意bit是从0开始计算,也就是第四个bit。
而关于 __device_start 和 __device_end 的定义如下:
所以此处可以看作是一个结构体数组,首元素指针为 __device_start ,最后一个元素指针为 __device_end,通过结构体指针累加,去调用每个结构体元素的name与入参比对。
这时候会有一个疑问,__device_start到__device_end里的数据是在什么时候被存进去的呢?ready状态又是什么时候被置位的呢?
起始和之前写过的RT-Thread那篇文章里提到的自动初始化一样,这部分设备信息,在编译时就已经确定。在gpio_nrfx.c中有:
此宏较为复杂,具体不再分析,其中内容与设备树生成的.h有关,基本流程是读取设备树内信息,把信息以结构体的形式填充在相应的段内。
需要注意的是在设备注册的时候,操作设备的一系列api就随函数指针传入设备信息内了:
右边的所有函数,最终还是调用了Nordic的底层库。
其实具体实现不明白也没关系,只需要会按照规则修改设备树文件,并会使用设备树宏就可以了,设备树相关宏都以 DT_ 开头。
gpio_pin_configure()
此函数使用简单,注意这里配置使用了 | FLAGS,是为了在默认配置的基础上去修改。
可以配置的选项包括输入输出配置,上下拉配置,中断配置等,具体配置可以在函数内查看:
熟悉GPIO的应该从名字就可以判断每个标志所代表配置含义。(在查看这部分代码时,经常会触发一个同时打开多个同名文件导致软件无响应bug,事实证明,一个新的东西总是伴随着新的bug)
gpio_pin_set()
关于引脚电平设置,在这里最终调用的函数为:
这里的port即dev,而dev的api则是在之前介绍的文件gpio_nrfx.c中设备初始化传入的api函数指针。最终调用的还是Nordic的底层库:
最后,不得不说zephyr是一个很复杂的轻量级系统。 面对市面上像ucos,freertos,rt-thread,zephyr等让人眼花缭乱的轻量级os,如何选择一个合适的os很重要。