zephyr构建编译过程学习
zephyr中是以Cmake管理源码和编译过程的,项目本身会把应用代码编译成build/app/libapp.a,最后和Zephyr系统一起链接成可执行文件。
zephyr通过Kconfig管理配置
zephyr内核对象学习
定时器
类似linux的定时器,
可以分别设置第一次到期时间和后续的周期触发时间,
可以注册到期回调和停止回调
还有一个计数状态,用于标记timer到期了多少次
duration:设定timer第一次到期的时间。
period: timer第一次到期后的触发时间间隔。
expiry:触发回调。
定时器的使用:
- 初始化定时器
void k_timer_init(struct k_timer *timer, k_timer_expiry_t expiry_fn, k_timer_stop_t stop_fn);
- 启动定时器
void k_timer_start(struct k_timer *timer, k_timeout_t duration, k_timeout_t period);
- 停止定时器
void k_timer_stop(struct k_timer *timer);
- 读取定时器状态
uint32_t k_timer_status_get(struct k_timer *timer);
读取定时器的状态,该状态表示自上次读取其状态以来定时器已到期的次数,每次读取后会重置状态为0。
- 等待定时器到期
uint32_t k_timer_status_sync(struct k_timer *timer);
调用这个函数会阻塞线程,直到定时器到期或者停止,调用这个函数会将定时器状态清零,另外不允许在中断处理函数中调用该函数,函数返回定时器的状态值。
- 获取定时器超时到期时的系统时间
k_ticks_t k_timer_expires_ticks(const struct k_timer *timer);
该函数返回定时器下一次到期时候的系统时间,以系统ticks为单位。如果定时器未运行,则返回当前系统时间。
- 获取定时器超时到期的剩余时间
k_ticks_t k_timer_remaining_ticks(const struct k_timer *timer)
计算运行的定时器下次过期前剩余的时间,如果定时器未运行,则返回0。
- 获取定时器超时到期前的剩余时间
uint32_t k_timer_remaining_get(struct k_timer *timer);
计算运行定时器下次到期前剩余的(近似)时间,以毫秒(ms)为单位。如果定时器未运行,则返回0。
另外还有一种定义和初始化定时器的方式:
静态定义并初始化定时器
#define K_TIMER_DEFINE(name, expiry_fn, stop_fn)
注意
因为timer的回调是在中断中执行,所以在回调函数中不能做耗时操作。
timer不能保证精确的定时,但其精度比k_sleep/k_usleep高,测量执行时间时不建议使用k_timer,建议读系统硬件时钟。
当timer触发回调后需要处理耗时操作时,可配合k_work使用,将耗时操作放在workqueue中执行
/* k_work回调函数,用于处理耗时操作 */
void work_handler(struct k_work *work)
{
while(int i=0, i<100, i++){
printk("do something \n");
}
}
/* 定义初始化一个k_work */
K_WORK_DEFINE(a_work, work_handler);
/* timer到期回调函数 */
static void timer_handler_expiry(struct k_timer *dummy)
{
counter++;
printk("counter %d \n", counter);
/*发送k_work信号量*/
k_work_submit(&a_work);
}
信号量
信号量是用于控制多个线程对一组资源的访问,使用信号量在生产者和消费者之间同步
- Zephyr的信号量在初始化时可以指定初始化计数值和最大计数值,生产者释放(give)信号量时计数值+1,但不会超过最大值,消费者获取(take)时计数值-1,直到为0。
- 每次信号量释放时都会引发调度。
- 如果多个线程都在等待信号量,新产生的信号量会被等待时间最长的最高优先级线程接收。
信号量的使用
- 初始化信号量
int k_sem_init(struct k_sem *sem, unsigned int initial_count, unsigned int limit);
- 获取信号量
int k_sem_take(struct k_sem *sem, k_timeout_t timeout);
- 释放信号量
void k_sem_give(struct k_sem *sem);
互斥量
互斥量本质应该和初始值和最大值为1的信号量相同;目的主要是为了提供对资源的独占访问(因为只有0和1,只有一个线程能拿到资源,所以就实现了独占访问)
- 互斥量只能用于线程中,不能用于中断(会引起阻塞,所以不能用于中断)
- 互斥量释放会引起调度(释放信号量也会引起调度)
- 引起阻塞之后可能会导致优先级翻转(那理论上信号量也会引起优先级翻转)
互斥量的使用
- 初始化互斥量
int k_mutex_init(struct k_mutex *mutex);
- 互斥量上锁(相当于获取信号量)
int k_mutex_lock(struct k_mutex *mutex, k_timeout_t timeout);
- 互斥锁解锁(相当于释放信号量)
void k_mutex_unlock(struct k_mutex *mutex);
轮询
轮询(poll)是一个比较特殊的内核对象,polling API 允许一个线程等待一个或者多个条件满足。支持的条件类型只能是内核对象,可以是Semaphore(信号量), FIFO(管道), poll signal(轮询)三种。
例如一个线程使用polling API同时等待多个semaphore,只要其中一个 semaphore 触发时 polling API 就会得到通知。
poll 具有以下特性:
- 当一个线程等待多个触发条件时,只要有一个条件满足 k_poll 就会返回。
- 当 Semaphore 或 FIFO 满足条件后, k_poll 只是接到通知返回,线程并未获取到 semaphore 或FIFO, 还需要使用代码主动获取。
轮询的使用
- 初始化轮询实例
void k_poll_event_init(struct k_poll_event *event, uint32_t type, int mode, void *obj);
初始化的时候,一次只能添加一个内存对象,event是数组指针,type是指后面obj的类型(信号量或者FIFO或者轮询信号,不论是这三个的哪一种,在这之前都要调用对应的初始化接口进行初始化),mode一般是notify_only
- 轮询接口
int k_poll (struct k_poll_event *events, int num_events, k_timeout_t timeout)
在一次释放之后,如果k_poll需要再次捕获该信号,需要先调用复位信号的接口进行复位,否则将无法再次释放;
如果用的是poll_signal,可以用下面的接口进行操作:
- 轮询信号初始化
void k_poll_signal_init(struct k_poll_signal *sig);
- 轮询信号释放
int k_poll_signal_raise(struct k_poll_signal *sig, int result);
- 复位轮询信号
void k_poll_signal_reset(struct k_poll_signal *sig);
- 检查轮询信号
void k_poll_signal_check(struct k_poll_signal *sig, unsigned int *signaled, int *result);
个人理解,应该是在k_poll轮询多个对象其中包含poll_signal时,用来确定是不是signal被捕获到了;如果需要判断其他内核对象(信号量或者FIFO),则需要主动判断k_poll接口中的struct k_poll_event *events参数的state是sem有效还是fifo_data有效;
线程分析器
线程分析器作为zephyr的一个调试工具,用来跟踪线程信息例如线程的堆栈大小使用情况、线程运行时的其他统计信息。
线程分析器的使用
个人理解,有三种使用方式:
- 将信息收集到某个回调中
void thread_analyzer_run(thread_analyzer_cb cb)
- 将信息直接打印出来
void thread_analyzer_print(void)
- 自动运行线程分析器
这种只需要在项目的prj.conf配置文件中打开以下配置:
# 启用线程分析器
CONFIG_THREAD_ANALYZER=y
# 使用PRINTK输出进行线程统计
CONFIG_THREAD_ANALYZER_USE_PRINTK=y
# 在内核中启用此选项以打印线程的名称
CONFIG_THREAD_NAME=y
# 自动运行线程分析器,使用此选项时,不需要向应用程序添加任何代码
CONFIG_THREAD_ANALYZER_AUTO=y
# 在自动模式下连续打印线程分析之间,模块休眠的时间单位(S)
CONFIG_THREAD_ANALYZER_AUTO_INTERVAL=5
工作队列(未详细研究)
工作队列(workqueue)是一个使用特定线程来运行工作项(work items)的内核对象,其方式为先进先出,通过调用工作项指定的函数来处理每个工作项。工作队列的典型应用是在中断或者高优先级线程中去分担部分工作到一个低优先级的线程中,其目的是减少中断或高优先级线程的处理时长,所以它不影响时间敏感的处理。
工作队列的使用
- 初始化工作队列
void k_work_queue_init(struct k_work_q *queue)
- 启动工作队列
void k_work_queue_start(struct k_work_q *queue, k_thread_stack_t *stack, size_t stack_size, int prio, const struct k_work_queue_config *cfg)
- 初始化工作结构
void k_work_init(struct k_work *work, k_work_handler_t handler)
- 提交工作项到工作队列
int k_work_submit(struct k_work *work)
日志输出
日志输出分为5个等级:
- LOG_LEVEL_NONE -- 不显示任何日志数据
- LOG_LEVEL_ERR -- 只显示error级别的日志
- LOG_LEVEL_WRN -- 只显示error、warning级别的日志
- LOG_LEVEL_INF -- 只显示error、warning、info级别的日志
- LOG_LEVEL_DBG -- 显示所有级别的日志
对应不同级别的API接口:
- LOG_ERR
- LOG_WRN
- LOG_INF
- LOG_DBG
- LOG_PRINTK
其中LOG_PRINTK不受log输出等级的限制,也就是不论啥等级都能输出
对应不同级别的hex数据输出API:
- LOG_HEXDUMP_ERR
- LOG_HEXDUMP_WRN
- LOG_HEXDUMP_INF
- LOG_HEXDUMP_DBG
使用前还需要调用以下宏:
#include <logging/log.h>
LOG_MODULE_REGISTER(LOG_MODULE_NAME, LOG_MODULE_LEVEL_DBG);
参数说明:
LOG_MODULE_NAME:用来指定该模块化log的名称
LOG_MODULE_LEVEL_DBG:用于指定该模块化log的输出级别,若此参数未定义,则默认为CONFIG_LOG_DEFAULT_LEVEL。
其他
zephyr也支持 USB协议栈,LVGL,SHELL,NVS,**文件系统(littlefs和fatfs)**等模块;
内核
FIFOs
FIFO 是一个内核对象,它实现了传统的先进先出 (first in first out) 队列,允许在线程和中断 中添加、移除任意尺寸的数据项。
队列是使用一个简单的单链表实现的。
FIFO 必须先初始化再使用。初始化时会将队列设为空。
FIFO 的数据项必须 4 字节向上对齐,这是因为每个数据项的首个4字节是内核保留的,它被当做指针用来指向队列中的下一个数据项。
因此,如果应用程序需要 N 字节的数据项时,实际需要 N+4(或N+8) 字节的内存。
如果使用 k_fifo_alloc_put() 添加数据的话,则不需要数据对齐或者保留空间的,这些额外的内存是临时从调用线程的资源池中分配的。
数据项可以被线程或者中断添加到 FIFO 中。如果有线程在等待从这个 FIFO 中取数据,这个数据项则直接被给予这个线程;否则,该项会被直接添加到 FIFO 的队列中去。对可入队的数据项的数量没有任何限制。
数据项可以被线程从 FIFO 中移除。如果该 FIFO 的队列为空,线程在这个 FIFO 上进行等待。多个线程可以同时在某个空 FIFO 上等待,当一个新的数据项被添加时,它会被给予优先级最高的、等待时间最久的线程。
中断服务函数虽然也可以从 FIFO 中移除数据,但是如果 FIFO 为空,中断服务函数中不能进行等待。
中断
内核支持中断嵌套。
中断服务函数在内核的中断上下文中执行。内核中断上下文具有自己的专用堆栈。如果使能了中断嵌套,中断上下文堆栈的大小,必须能够满足多个并发ISR的中断处理
很多内核接口只能在线程中使用,而不能在中断中使用。如果一个函数允许被线程和中断调用,可以用 k_is_in_isr()来判断上下文是否在中断中,从而进行不同的处理
中断屏蔽
在线程中可以使用中断锁(IRQ lock)暂时屏蔽中断请求处理。即使中断锁已经生效,但仍可以再次调用,所以调用者不需要知道是否已经生效。线程释放锁的次数,必须与调用锁的次数相同,才能真正的恢复该线程运行时的中断响应。
中断锁是线程特有的。如果线程A进行了锁中断的操作后再睡眠(睡眠若干毫秒),线程A被挂起,准备执行B线程,那么线程A中的中断锁就不再生效。这意味着在执行线程B时,中断仍然可以被触发,除非在线程B中调用中断锁。当再次执行线程A,内核将重新使能中断锁,确保线程A不会被中断直到线程A释放中断锁。如果线程A没有睡眠,但此时一个高优先级线程B就绪,中断锁将禁止任何可能的抢占。只有线程A释放了中断锁,线程B才有可能被调度运行。
还有另一种做法,在线程中临时禁用某个中断请求,这样当对应中断信号产生时,不会执行中断服务函数。但在线程处理后,尽快重新使能该中断请求,这样才能恢复其中断处理的响应。禁用中断请求可以防止所有线程被中断抢占,而不仅仅是禁用IRQ的线程。
零延迟中断
内核允许对延迟有要求的中断在使用中断锁后仍能触发。这些中断被定义为零延迟中断(Zero Latency Interrupts)。
零延迟中断用于直接管理硬件中断,不会经过Zephyr内核。在零延迟中断服务函数中使用内核API,需要用户去保证API使用的正确性。零延迟中断不得修改从正常Zephyr上下文调用的内核API所检查的任何数据,也不得产生需要同步处理的异常(例如内核panic)。
零延迟中断需要使能CONFIG_ZERO_LATENCY_IRQS。另外,需要在IRQ_CONNECT 或 IRQ_DIRECT_CONNECT中配置为 IRQ_ZERO_LATENCY 。
中断服务函数执行完成后,内核会执行一次 上下文切换。
互斥量
多个线程可以同时等待某个被锁定的互斥量。当该互斥量被解锁后,它会被优先级最高的、等待时间最久的线程所使用。
内核支持优先级继承,以解决优先级翻转问题;
信号量
信号量可以在线程或中断中进行释放(give) 。释放信号量会使其计数递增,直到计数达到上限。
线程可以获取(take) 信号量。获取信号量时其计数会递减,除非信号量无效(例如为零)。当信号量不可用时,线程可以等待,直到获取到信号量。多个线程可以同时等待某个无效的信号量。当信号量可用时,它会被优先级最高的、等待时间最久的线程获取到。
内核允许在中断服务函数中获取信号量,但当信号量无效时,不能在中断服务函数中阻塞等待该信号量。
堆管理
zephyr允许线程动态分配内存
同步堆分配器
创建堆
静态定义堆对象
static K_HEAP_DEFINE(test_heap, 1024 * 2);
使用k_heap_init()创建堆对象
static struct k_heap test_heap; /* define the heap object */
uint8_t test_heap_pool[1024 * 2]; /* define the heap region */
k_heap_init(&test_heap, test_heap_pool, sizeof(test_heap_pool));
分配和释放内存
使用 k_heap_alloc() 从堆中分配内存,使用该接口需要传入堆对象(k_heap)和需要分配的内存大小。
这个函数类似标准C里面的 malloc() ,返回NULL表示分配失败。
k_heap_alloc() 支持阻塞操作,允许线程睡眠直到内存可用。k_timeout_t 表示阻塞的时间,可选为 K_NO_WAIT 或 K_FOREVER 之一。
k_heap_alloc() 分配的内存必须要通过 k_heap_free() 来释放,与标准C的 free() 相似,传入的参数必须是NULL 或者 k_heap_alloc() 返回的地址。释放 NULL 值不会有任何影响。
k_heap 相关的函数是线程安全的。
定时器
定时器类似Linux定时器,分为第一次到期时间和后续运行周期
如果需要测量线程执行操作所需的时间,它可以直接读取系统时钟或硬件时钟,而不是使用定时器。
工作队列线程
无论优先级如何,工作队列线程在完成一个工作项之后,会主动让出CPU一次(个人理解),防止饿死其他线程
工作项
每个工作项都被分配一个处理函数(handler function),这是工作队列线程在执行工作项时执行的函数。处理函数唯一的形参就是工作项句柄,可在工作项句柄中获取工作项状态。
工作项必须在使用前初始化。初始化时工作项时会分配处理函数,然后标记为未完成。
在ISR或线程中,工作项可被添加到工作队列中
还未执行的工作项可以被取消,内核提供获取工作项状态的API接口和取消工作项的API接口
处理函数可以调用任何适用于线程的内核API。但是,带阻塞相关的接口(如k_sem_take)需要斟酌使用。这是因为,在当前处理函数执行完成之前,工作队列无法处理其队列中的后续工作项。
根据实际需求,选择性使用处理函数的形参。如果处理函数需要更多的信息,可以将工作项句柄作为结构体成员。处理函数调用CONTAINER_OF 宏获取到结构体地址,从而获取更多的信息。
一个工作项目通常被初始化一次,在需要执行工作项时提交给工作队列。提交一个已经在工作队列中工作项是不会产生任何影响,不会影响该工作项在工作队列中的位置,并且只会执行一次。
在工作项执行完成以后,工作项允许重新分配处理函数,在不同阶段时分配不同的处理函数,能避免工作队列中有冗余的工作项。
可延迟工作
如果在中断或线程中需要调度一个工作项在指定周期后执行,那么通过可延迟的工作项(delayable work item)在将来的某个时间提交到工作队列中完成。
一个可延迟工作项结构在标准工作项结构的基础上额外添加字段,用于描述何时将工作项添加到指定的队列。
可延迟工作项的初始化和排到工作队列中的行为与标准工作项一致,只是调用的内核API不同。
当发出该工作队列的调度请求后,内核启动超时机制,在指定的延迟后被触发。一旦超时发生,内核就会把工作项目提交给指定的工作队列,工作项一直被排在工作队列中,以标准工作项方式被处理。
个人理解,可延迟工作就是把正常工作项等一会再提交至工作队列
内核定义了系统工作队列,用于任何应用或者内核代码。
只有在系统工作队列无法满足需求的时候,才建议新创建一个工作队列, 因为每个工作队列都会占用较大的内存。如果新的工作项执行阻塞性操作,会使其他系统工作队的处理延迟到不可接受的程度。那么这种情况下,可以新建一个工作队列。
消息队列
消息队列的环形缓冲区必须与N字节边界对齐,其中N是2的幂(即1,2,4,8,……)。为了确保存储在环形缓冲区中的消息与边界对齐,数据项大小也必须是N的倍数量。
zephyr蓝牙协议栈学习
简介
zephyr主要支持BLE,对BR/EDR仅提供有限的支持
core5.3中BLE功能几乎全部支持,包括LE audio和mesh;
BR/EDR仅支持部分,GPA,L2CAP,RFCOMM,SDP,(不过看到zephyr代码里也有HF,A2DP,AVDTP等)
zephyr可以仅被配置为controller或者host,也可以配置为既有controller也有host
zephyr仅做host时,支持跟多个controller同时通信
源码树层次
subsys/bluetooth/host
这里是host stack。处理HCI命令和事件地方,L2CAP,ATT,SMP等核心协议也在这里
subsys/bluetooth/controller
蓝牙控制器实现。实现HCI的控制器端,链路层以及对无线电收发器的访问
include/bluetooth/
公共API头文件。这些是应用程序需要包含的头文件,以便使用蓝牙功能
drivers/bluetooth
HCI传输层驱动。每个HCI传输层都需要自己的驱动程序。(三线uart或者5线uart,usb,spi等)
samples/bluetooth
蓝牙实例代码。
test/bluetooth
测试应用程序。这些应用程序用于验证蓝牙堆栈的功能。
doc/guides/bluetooth
额外的文档,比如PICS文档
HOST
GAP通过定义BLE使用的四个不同角色来简化蓝牙LE访问:
面向连接的角色:
- 外围设备(例如智能传感器,通常具有有限的用户界面)
- 中央设备(通常是移动电话或PC)
无连接的角色:
- 广播者(发送BLE广告,例如智能信标)
- 观察者(扫描BLE广告)
在面向连接的角色中,中央设备隐式的启用观察者角色,外围设备隐式的启用广播者角色
注册gatt service的方法
使用BT_GATT_SERVICE_DEFINE宏
实际管理单位应该是attr
/**
* @brief Statically define and register a service.
*
* Helper macro to statically define and register a service.
*
* @param _name Service name.
*/
#define BT_GATT_SERVICE_DEFINE(_name, ...) \
const struct bt_gatt_attr attr_##_name[] = { __VA_ARGS__ }; \
const STRUCT_SECTION_ITERABLE(bt_gatt_service_static, _name) = \
BT_GATT_SERVICE(attr_##_name)
/** @brief GATT Attribute structure. */
struct bt_gatt_attr {
/** Attribute UUID */
const struct bt_uuid *uuid;
bt_gatt_attr_read_func_t read;
/** Attribute write callback */
bt_gatt_attr_write_func_t write;
/** Attribute user data */
void *user_data;
/** Attribute handle */
uint16_t handle;
/** @brief Attribute permissions.
*
* Will be 0 if returned from ``bt_gatt_discover()``.
*/
uint16_t perm;
};