sequenceDiagram App-->>Host: bt_enable() Host-->HCI: bt_hci_open() App-->>Host: bt_le_scan_cb_register() App-->>Host: bt_le_per_adv_sync_cb_register() App-->>Host: bt_le_scan_start() Host-->>HCI: BT_HCI_OP_LE_SET_EXT_SCAN_PARAM HCI-->>ULL_SCAN: ll_scan_params_set() ULL_SCAN-->>HCI: OK or FAIL? HCI-->>Host: cmd_complete_status() Host-->>HCI: BT_HCI_OP_LE_SET_EXT_SCAN_ENABLE HCI-->>ULL_SCAN: ll_scan_enable() ULL_SCAN-->>ULL_SCAN: scan = ull_scan_is_disabled_get(SCAN_HANDLE_1M); ULL_SCAN-->>ULL_SCAN: duration_period_setup() ULL_SCAN-->>ULL_Filter: ull_filter_scan_update() ULL_SCAN-->>ULL_Filter: ull_filter_rpa_update() ULL_SCAN-->>ULL_SCAN: ull_scan_enable() ULL_SCAN-->>HCI: OK or FAIL? HCI-->>Host: cmd_complete_status() ULL_SCAN-->>Ticker: ticker_start() Ticker-->>ULL_SCAN: tickcer_cb() ULL_SCAN-->>Mayfly: mayfly_enqueue() Mayfly-->>LLL_SCAN: lll_scan_prepare() LLL_SCAN-->>LLL_SCAN: prepare_cb() LLL_SCAN-->>+LLL_SCAN: common_prepare_cb() LLL_SCAN-->>Radio: radio_reset() LLL_SCAN-->>Radio: radio_tx_power_set() LLL_SCAN-->>Radio: radio_phy_set() LLL_SCAN-->>Radio: radio_pkt_configure() LLL_SCAN-->>Radio: radio_pkt_rx_set() LLL_SCAN-->>Radio: radio_aa_set() LLL_SCAN-->>Radio: radio_crc_configure() LLL_SCAN-->>Radio: radio_freq_chan_set(chan) LLL_SCAN-->>Radio: radio_whiten_iv_set(chan) LLL_SCAN-->>Radio: radio_isr_set(**isr_rx**, lll); LLL_SCAN-->>Radio: radio_tmr_tifs_set() LLL_SCAN-->>Radio: radio_filter_configure() LLL_SCAN-->>Radio: radio_ar_configure() LLL_SCAN-->>Radio: radio_tmr_start() LLL_SCAN-->>Radio: radio_tmr_end_capture() LLL_SCAN-->>Radio: radio_rssi_measure() LLL_SCAN-->>-Ticker: ticker_start(TICKER_ID_SCAN_STOP) Radio-->>+LLL_SCAN: isr_rx() LLL_SCAN-->>LLL_SCAN: lll_scan_aux_addr_match_get() LLL_SCAN-->>LLL_SCAN: lll_scan_isr_rx_check() LLL_SCAN-->>LLL_SCAN: isr_rx_pdu() LLL_SCAN-->>LLL_SCAN: isr_rx_scan_report() NODE_RX_TYPE_EXT_1M_REPORT opt Active scanner 需要发送Scan Request LLL_SCAN-->>Radio: radio_tmr_tifs_set() LLL_SCAN-->>Radio: radio_switch_complete_and_rx(0) LLL_SCAN-->>LLL_SCAN: isr_rx_scan_report() LLL_SCAN-->>Radio: radio_pkt_tx_set,PDU_ADV_TYPE_SCAN_REQ with radio_pkt_scratch_get(); LLL_SCAN-->>Radio: radio_tmr_end_capture(); LLL_SCAN-->>Radio: radio_isr_set(isr_tx, lll) Radio-->>+LLL_SCAN: isr_tx(void *param) LLL_SCAN-->>Radio: radio_switch_complete_and_disable() LLL_SCAN-->>Radio: radio_pkt_rx_set() LLL_SCAN-->>Radio: radio_tmr_hcto_configure() LLL_SCAN-->>Radio:radio_rssi_measure() LLL_SCAN-->>-Radio: radio_isr_set(isr_rx, param) 切换到RX end opt Passive scanner or scan responses ,LLL处理EXT_IND LLL_SCAN-->>LLL_SCAN_AUX: lll_scan_aux_setup() LLL_SCAN_AUX-->>+LLL_SCAN_AUX: lll_scan_aux_isr_aux_setup() LLL_SCAN_AUX-->>Radio: radio_isr_set(lll_scan_aux_isr_aux_setup) LLL_SCAN_AUX-->>-Radio: radio_disable() end LLL_SCAN-->>ULL: ull_rx_put_sched() NODE_RX_TYPE_EXT_1M_REPORT ULL-->>ULL: ull_rx_sched() ULL-->>Mayfly: mayfly_enqueue() Mayfly-->>ULL: rx_demux() ULL-->>ULL: rx_demux_rx(link, rx); ULL-->>ULL_SCAN_AUX: ull_scan_aux_setup(NODE_RX_TYPE_EXT_1M_REPORT) ULL_SCAN_AUX-->>ULL_Sync: ull_sync_setup() if sync is created ULL_Sync-->>Ticker: ticker_start() Ticker-->>ULL_Sync: ticker_cb() ULL_Sync-->>Mayfly: mayfly_enqueue() Mayfly-->>LLL_Sync: lll_sync_create_prepare() LLL_Sync-->>LLL_Sync: create_prepare_cb() LLL_Sync-->>LLL_Sync: prepare_cb_common() LLL_Sync-->>Radio: radio_isr_set(**isr_rx_adv_sync_estab**, lll) Radio-->>LLL_Sync: isr_rx_adv_sync_estab() opt SYNC_STAT_ALLOWED LLL_Sync-->>LLL_Sync: isr_rx() LLL_Sync-->>LLL_SCAN_AUX: lll_scan_aux_setup() LLL_SCAN_AUX-->>Radio: radio_isr_set(isr_aux_setup) LLL_Sync-->>ULL: ull_rx_put(NODE_RX_TYPE_SYNC, SYNC_STAT_ALLOW); LLL_Sync-->>ULL: ull_rx_sched() 通知ULL ULL-->>ULL: rx_demux_rx(NODE_RX_TYPE_SYNC) ULL-->>ULL_Sync: ull_sync_established_report ULL_Sync-->>ULL_SCAN_AUX: ull_scan_aux_setup(NODE_RX_TYPE_SYNC_REPORT) 如果LLL已经启动,ULL就不会再启动ticker ULL_SCAN_AUX-->>ULL_Sync: ull_sync_setup() if sync is created. ULL_SCAN_AUX-->>Ticker: ticker_start() 如果LLL没有启动AUX,ULL就启动ticker Ticker-->>ULL_SCAN_AUX: ticker_cb() ULL_SCAN_AUX-->>Mayfly: mayfly_enqueue() Mayfly-->>LLL_SCAN_AUX: lll_scan_aux_prepare() LLL_SCAN_AUX-->>LLL_SCAN_AUX: prepare_cb() opt 如果有前序AUX,那后面就是CHAIN IND LLL_SCAN_AUX-->>LLL_SCAN_AUX: lll_sync_aux_prepare_cb() 是PA train LLL_SCAN_AUX-->>Radio: radio_isr_set(isr_rx_aux_chain, lll) end opt 如果没有前序AUX LLL_SCAN_AUX-->>Radio: radio_isr_set(isr_rx_ull_schedule, lll_aux) if NOT PA train. LLL_SCAN_AUX-->>LLL: lll_prepare_done() end Radio-->>LLL_Sync: isr_aux_setup() LLL_Sync-->>Radio: radio_isr_set(isr_rx_aux_chain, lll) LLL_Sync-->>Radio: radio_tmr_start_us() LLL_Sync-->>Radio: radio_tmr_end_capture() Radio->>LLL_Sync: isr_rx_aux_chain() LLL_Sync-->>LLL_Sync: isr_rx(NODE_RX_TYPE_EXT_AUX_REPORT) end opt SYNC_STAT_TERM LLL_Sync-->>ULL: ull_rx_put_sched(SYNC_STAT_TERM) 终结 end opt LLL 层来处理AUX Radio-->>+LLL_SCAN_AUX: lll_scan_aux_isr_aux_setup() LLL_SCAN_AUX-->>LLL: lll_isr_status_reset() LLL_SCAN_AUX-->>Radio: radio_phy_set() LLL_SCAN_AUX-->>Radio:radio_pkt_configure() LLL_SCAN_AUX-->>LLL: lll_chan_set() LLL_SCAN_AUX-->>Radio: radio_pkt_rx_set() LLL_SCAN_AUX-->>Radio: radio_isr_set(isr_rx_lll_schedule, node_rx) LLL_SCAN_AUX-->>Radio: radio_tmr_tifs_set(EVENT_IFS_US) LLL_SCAN_AUX-->>Radio: radio_switch_complete_and_tx(phy_aux, 0, phy_aux, 1); LLL_SCAN_AUX-->>Radio: radio_tmr_start_us(0, aux_start_us) LLL_SCAN_AUX-->>Radio: radio_tmr_hcto_configure(hcto) LLL_SCAN_AUX-->>Radio: radio_tmr_end_capture() LLL_SCAN_AUX-->>-Radio: radio_rssi_measure() Radio-->>LLL_SCAN_AUX: isr_rx_lll_schedule() LLL_SCAN_AUX-->>LLL_SCAN_AUX: isr_rx() LLL_SCAN_AUX-->>LLL_SCAN_AUX: isr_rx_pdu() opt ADV是Scannable的 LLL_SCAN_AUX-->>Radio: radio_pkt_tx_set() PDU_ADV_TYPE_SCAN_REQ LLL_SCAN_AUX-->>Radio: radio_isr_set(isr_tx_scan_req_lll_schedule) LLL_SCAN_AUX-->>ULL: ull_rx_put_sched(),NODE_RX_TYPE_EXT_AUX_REPORT Radio-->>+LLL_SCAN_AUX: isr_tx_scan_req_lll_schedule() LLL_SCAN_AUX-->>LLL_SCAN_AUX: isr_tx() LLL_SCAN_AUX-->>-Radio: radio_isr_set(isr_rx_lll_schedule()) Radio-->>LLL_SCAN_AUX: isr_rx_lll_schedule() LLL_SCAN-->>LLL_SCAN: isr_rx() LLL_SCAN-->>LLL_SCAN: isr_rx_pdu() LLL_SCAN-->>LLL_SCAN: lll_scan_aux_setup() LLL_SCAN-->>Radio: radio_isr_set(lll_scan_aux_isr_aux_setup) LLL_SCAN-->>ULL: ull_rx_put_sched(NODE_RX_TYPE_EXT_AUX_REPORT) end end LLL_SCAN-->>Radio: radio_isr_set(isr_done, lll); LLL_SCAN-->>-Radio: radio_disable() Radio-->>+LLL_SCAN: isr_done() LLL_SCAN-->>Radio: radio_rx_enable() LLL_SCAN-->>-Radio: radio_tmr_end_capture() LLL_SCAN-->>LLL_SCAN: lll_prepare_done() Ticker-->>LLL_SCAN: ticker_stop_cb() LLL_SCAN-->>Mayfly: mayfly_enqueue() Mayfly-->>LLL_SCAN: lll_disable() loop Alaways App-->>App: k_sem_take(&sem_per_adv, K_FOREVER); App-->>Host: bt_le_per_adv_sync_create() Host-->>HCI: BT_HCI_OP_LE_PER_ADV_CREATE_SYNC HCI-->>ULL_Sync: ll_sync_create() ULL_Sync-->>ULL_Sync: scan->periodic.sync = sync,设定Sync Host-->>HCI: BT_HCI_OP_LE_SET_PER_ADV_RECV_ENABLE HCI-->>ULL_Sync: ll_sync_recv_enable() Host-->>HCI: BT_HCI_OP_LE_PER_ADV_TERMINATE_SYNC HCI-->>ULL_Sync: ll_sync_terminate() App-->>App: k_sem_take(&sem_per_sync, TIMEOUT_SYNC_CREATE) end
Zephyr Ticker 介绍
在Zephyr bluetooth controller中,每一个procedure 要想拿到Radio去收发数据,必须要闯过两道关卡:ULL层的Ticker和LLL层的Prepare, 本文主要介绍Ticker的部分。
Ticker 负责仲裁Link Layer中各个procedure对RF资源的申请,用定义好的Rule来保证这些procedure都能被童叟无欺的执行到。
但实际上这个module是个完全独立的module,跟蓝牙的行为没有什么关系,也可以拿来做他用。
在Ticker里有三大角色:
- Node:是Ticker的调度单元,里面的关键信息是到期时间(ticks_to_expire)及申请占用RF的时间(ticks_slot)。
User:是Node的用户,总共有4个,从字面意思看是跟优先级相关,但实际上没有优先级的概念。
1
2
3
4#define TICKER_USER_ID_LLL MAYFLY_CALL_ID_0
#define TICKER_USER_ID_ULL_HIGH MAYFLY_CALL_ID_1
#define TICKER_USER_ID_ULL_LOW MAYFLY_CALL_ID_2
#define TICKER_USER_ID_THREAD MAYFLY_CALL_ID_PROGRAMUser Operation:User希望要做的操作。
1
2
3
4
5
6
7
8
9#define TICKER_USER_OP_TYPE_NONE 0
#define TICKER_USER_OP_TYPE_IDLE_GET 1
#define TICKER_USER_OP_TYPE_SLOT_GET 2
#define TICKER_USER_OP_TYPE_PRIORITY_SET 3
#define TICKER_USER_OP_TYPE_START 4
#define TICKER_USER_OP_TYPE_UPDATE 5
#define TICKER_USER_OP_TYPE_YIELD_ABS 6
#define TICKER_USER_OP_TYPE_STOP 7
#define TICKER_USER_OP_TYPE_STOP_ABS 8
这三者的关系,我们用下面的图形来举例:
- 带箭头的蓝色横线是一个时间线,展现已经处于scheduled状态的所有节点的先后关系,这些节点等着他们的expiry时间到后,就可以执行他们的expiry callback函数,完成一个轮回。
- ticks_current是所有还未到期的节点的一绝对参考时间,第一个节点的expiry时间expiry1是相对于ticks_current,后面每个节点的expiry时间都是相对于前一个节点的expiry时间。
- 地标所指处为当前时间,可见当前的节点都还未过期。
- 蓝色Node #4 为待插入的节点,看起来他的slot有跟节点 #2 冲突。虽然#4 的ticks_slot有跟#2 的expiry时间冲突,但还是会插入成功。在#4 的expiry时间到期的时候才会做冲突处理,确定#2和#4 到底谁优先。
在理解Ticker所有时间相关的变量的时候,一定要注意该时间是绝对时间还是相对时间,如果是相对时间,是相对于谁,只要搞清楚这个,基本上能理解整个流程。
- 所有Node放在一个大数组里,由两个指针ticker_id_head 和insert_head来管理他们之间的关系。
- ticker_id_head 把所有可以被scheduled的Node(橘色)都串起来,这个list可以叫做scheduled list,第一个节点的expiry时间会送到RTC timer里,timer到期后就开始执行这个节点的callback。
- insert_head把所有待定的要进入sheduled list的节点(蓝色)串起来。这些节点会被 ticker_job_list_insert() 逐个插入到scheduled list,交由thread_id_head统领。
两个关键函数 相互配合完成调度
- ticker_job() 的目的是把所有希望得到调度的节点都欢送到Scheduled list里,它的操作对象有三个:
- 用
ticker_job_list_manage()
处理所有User的所有的OP中的Update 和 Stop 操作. - 用
ticker_job_worker_bh()
处理Scheduled list中所有的过期节点,这些节点可能还需要再进入Scheduled list中, ticker_work()不会判定已经expiry 的节点是否要再次进入Scheduled list中,是交由该函数处理,从函数名也能初见端倪,bh表示bottom half,就是后半部分,是ticker_worker的后半部分。 - 用
ticker_job_list_insert()
函数处理所有User的所有的operation中的START操作以及 Insert_head中的节点,这些节点是由前两步的操作带入的。 - 如果ticker_id_head有变,就重新把新的expiry时间设置到RTC。
- 用
flowchart TD A[start] -->B(ticker_job_list_manage) B --> C{flag_elapsed} C -->|Yes| D[ticker_job_worker_bh] D --> E[ticker_job_list_insert] C --> |No| E E --> F{ticker_id_head changes?} F --> |Yes| G[ticker_job_compare_update] F --> |No| H G --> H[Schedule job_worker] H --> I[End]
- ticker_worker() 目的是执行ticker_id_head 指向的节点的expiry callback,至于该Node要不要再次进入scheduled List,这里不管。在Node 插入schedule List的时候并不会针对ticks_slot做冲突检测,这样的话必须在这个函数里执行的时候做冲突检测,会影响执行效能。
graph TD A[Start] --> B(Calculate ticks_elapsed) B --> C{NextNode?} C -->|Yes| D{Expiry?} C -->|No| Job D -->|Yes| State{state == 0} D -->|No| Job State -->|Yes| Collide[ticker_resolve_collision] State -->|No| C Collide -->IsWin[Win?] IsWin -->|Yes| Callback[timeout_callback] IsWin -->|No| Skip_check[Skip?] Skip_check -->|Yes|C Skip_check -->|No|SetAsShallowExpiry SetAsShallowExpiry -->Callback Callback --> Job Job[Schedule a ticker_job] Job --> End End[End]
为了解决在执行callback的时候才做冲突出来带来的效率问题,有了另外一个版本(
CONFIG_BT_TICKER_LOW_LAT)的ticker_job_list_insert() 函数,该)函数会将insert_head里所有的节点尝试加入ticker_id_head,在加入的过程中,会有一些冲突处理机制如下图: - 特别注意Node #5 是没有slot的,它不占用RF,即使它的expiry时间被 #7 overlap到,但它跟#7并不冲突。
ticker_job_enqueue() 中的冲突处理:
Ticker中各个Compile option的作用:
- CONFIG_BT_TICKER_EXT:多了一个 ticks_slot_window,Adv对expiry的时间不需要严格精确,那么就把它的expiry时间前后稍微挪动下,如果能避免冲突,那它就能得到更早的执行。
- CONFIG_BT_TICKER_SLOT_AGNOSTIC:每个node都没有ticks_slot,完全把ticker拿来当timer来用,可以用一个RTC的HW timer来模拟多个SW timer;
- CONFIG_BT_TICKER_LOW_LAT:在Node进入schedule list时做collision 处理,而不是在expiry的时候。
几个问题:
- 假如某种原因,ticker_worker() 被耽误了,导致一次timeout,有好几个node的callback都被执行到了,那这些callback都要吃RF,他们之间的冲突又是怎么解决?
- Shallow Expiry时,Node 无论如何都要expiry,冲突时在那里解决的?
Nordic RTC 函数说明
Nordic Semiconductor的 nrf_rtc_cc_set
函数用于配置 RTC(实时时钟)的比较通道(Compare Channel),以便在 RTC 的计数器达到预设值时触发事件或中断。以下是该函数的参数说明及使用详解:
函数原型
1 | void nrf_rtc_cc_set(NRF_RTC_Type *p_rtc, uint32_t cc_channel, uint32_t cc_value); |
参数说明
参数 | 类型 | 说明 |
---|---|---|
p_rtc |
NRF_RTC_Type * |
指向 RTC 外设实例的指针。例如:NRF_RTC0 , NRF_RTC1 , NRF_RTC2 (具体取决于芯片型号)。 |
cc_channel |
uint32_t |
比较通道的编号,取值范围:0 到 NRF_RTC_CC_COUNT - 1 (通常为 0~3,共4个通道)。 |
cc_value |
uint32_t |
比较值(24位有效值),当 RTC 计数器(COUNTER )达到此值时触发比较事件。取值范围:0 ~ 0x00FFFFFF 。 |
功能详解
RTC 实例选择(
p_rtc
)
Nordic 芯片通常有多个 RTC 实例(如 nRF52 系列有 RTC0、RTC1、RTC2),需根据实际需求选择:- RTC0:常用于协议栈(如蓝牙 SoftDevice)。
- RTC1/RTC2:推荐用户自定义任务(如定时唤醒、周期性事件)。
- 示例:
1
nrf_rtc_cc_set(NRF_RTC1, 0, 32768); // 使用 RTC1 的通道0,设置比较值为32768
比较通道(
cc_channel
)- 每个 RTC 实例有多个比较通道(通常为4个),可独立配置。
- 注意:避免与其他模块(如协议栈或库)占用同一通道。例如,蓝牙协议栈可能默认使用 RTC0 的通道0。
比较值(
cc_value
)- RTC 计数器为 24位,因此
cc_value
最大为0x00FFFFFF
(16,777,215)。 时间计算:
实际时间与 RTC 的时钟源和分频系数相关。例如:若 RTC 时钟源为 32.768 kHz,分频系数为 1(
PRESCALER = 0
),则:1
21 秒 = 32768 个计数周期
cc_value = 目标时间(秒) * 32768若设置
cc_value = 32768
,则比较事件将在 1 秒后触发。
- RTC 计数器为 24位,因此
使用示例
场景:配置 RTC1 的通道0,在 5 秒后触发中断。
1 | #include "nrf_drv_rtc.h" |
注意事项
计数器溢出
RTC 是 24 位计数器,若cc_value
小于当前计数器值,需等待计数器溢出后重新计数到cc_value
才会触发事件。中断处理
在中断服务程序(如
rtc_handler
)中需清除事件标志:1
2
3
4
5
6void rtc_handler(nrf_drv_rtc_int_type_t int_type) {
if (int_type == NRF_DRV_RTC_INT_COMPARE0) {
nrf_rtc_event_clear(NRF_RTC1, NRF_RTC_EVENT_COMPARE_0);
// 处理逻辑
}
}若未清除事件标志,可能导致中断持续触发。
分频系数(PRESCALER)
RTC 的时钟频率由分频系数决定,需在初始化时配置:1
rtc_config.prescaler = 31; // 32.768 kHz / (31 + 1) = 1024 Hz
多通道协同
可配置多个比较通道实现复杂定时逻辑(如周期触发、超时检测)。
常见问题
Q1:设置了
cc_value
,但未触发事件?- 检查 RTC 是否已启动(
NRF_RTC_TASK_START
)。 - 确认中断已启用且未与其他模块冲突。
- 检查 RTC 是否已启动(
Q2:如何计算
cc_value
对应的时间?- 公式:
1
cc_value = (目标时间秒数) * (RTC时钟频率 / (PRESCALER + 1))
- 公式:
总结
nrf_rtc_cc_set
是 Nordic RTC 外设的核心函数,通过配置比较通道实现精准定时。使用时需注意时钟源、分频系数及中断处理,适用于低功耗定时任务(如传感器采样、蓝牙连接间隔等)。
Nordic的DPPI 和 PPI
本文由DeepSeek生成,在此做记录。
在 Nordic 半导体芯片(如 nRF 系列)的上下文中,DPPI(Distributed Programmable Peripheral Interconnect) 和 PPI(Programmable Peripheral Interconnect) 是两种硬件事件-任务路由机制,用于实现外设间的直接交互,减少 CPU 干预。以下是两者的核心区别:
1. 功能与架构
PPI
- 固定通道:PPI 使用预定义的通道(如 16 或 32 个通道),每个通道需手动配置事件源和任务目标。
- 静态绑定:事件与任务的绑定需通过代码显式设置,例如通过
nrf_ppi_channel_endpoint_setup
函数将无线电事件与定时器任务关联。 - 应用场景:适用于简单的硬件自动化,如超时控制或事件触发任务(如定时器捕获或无线电启停)。
DPPI
- 动态通道分配:DPPI 支持动态分配通道资源,允许更灵活的事件-任务映射,减少通道数量限制。
- 广播模式:单个事件可同时触发多个任务,通过“订阅-发布”机制实现多外设协同工作。
- 应用场景:适用于复杂系统,如多协议无线通信(蓝牙 + Thread)或需多外设联动的低功耗场景。
2. 配置方式
PPI
需手动配置通道的 事件源(Event) 和 任务目标(Task),例如:
1
2
3
4
5
6nrf_ppi_channel_endpoint_setup(
NRF_PPI,
HAL_RADIO_RECV_TIMEOUT_CANCEL_PPI,
(uint32_t)&NRF_RADIO->EVENTS_ADDRESS,
(uint32_t)&EVENT_TIMER->TASKS_CAPTURE[1]
);此处将无线电的地址匹配事件绑定到定时器的捕获任务。
DPPI
通过 订阅列表(Subscribe List) 和 发布列表(Publish List) 动态管理事件与任务的关联,例如:
1
nrf_dppi_subscribe_enable(NRF_DPPIC, EVENT_CHANNEL, TASK_GROUP);
允许一个事件触发多个任务组,减少代码冗余。
3. 性能与资源占用
PPI
- 低延迟:硬件直接路由,无 CPU 干预,适用于实时性要求高的场景。
- 资源受限:通道数量固定,需避免通道冲突(如多个外设竞争同一通道)。
DPPI
- 高扩展性:支持动态通道分配和广播机制,适合多任务并发场景。
- 低功耗优化:通过事件广播减少重复配置,降低功耗(如传感器数据批量处理)。
4. 典型应用对比
场景 | PPI 适用性 | DPPI 适用性 |
---|---|---|
单事件触发单任务 | ✅(如超时关闭无线电) | ✅(但资源利用率低) |
多事件触发多任务 | ❌(需多个通道) | ✅(动态订阅) |
低功耗传感器数据采集 | ❌(需频繁配置通道) | ✅(广播模式省电) |
多协议无线通信同步 | ❌(通道资源不足) | ✅(灵活路由) |
总结
- PPI:适合简单、固定的硬件自动化需求,配置直接但扩展性有限。
- DPPI:针对复杂系统设计,提供动态资源分配和多任务协同能力,是 Nordic 新一代芯片(如 nRF53 系列)的核心特性。
如需进一步了解具体芯片的实现细节,可参考 Nordic 官方文档(如《nRF5 SDK》或《nRF Connect SDK》)。
Zephyr的net_buf机制
Zephyr 作为Linux基金会推的物联网操作系统,里面有很多linux的影子,在github上有10K的star量,跟rt-thread差不多,值得深究一把。
按照Zephyr官网的步骤一步一步的把Zephyr的SDK build过,这期间为了解决各种问题,顺带买了阿里云的无影云电脑,完美解决了我手头这台9年老mac系统旧、速度慢的问题,有云电脑的加持感觉这台mac再战10年也没啥问题。
Zephyr的memory机制里有一个net_buf, 专门用在协议栈里各个层级来打包传输数据。net_buf 管理的buffer总共有三个来源:
- 一块固定的buffer
- 几个固定长度的buffer组成的数组
- 来自Heap的memory
其中第二项在bluetooth subsys里用的最多,有个专门的名字叫做 net buffer pool,这个pool管理了两个重要数组:
- 一个用来存放所有的net_buf,
- 一个是存放net_buf管理的buffer的来源
数据结构如下图所示:
- Pool的 _bufs 指针指向net_buf数组,数组里的net_buf在用过被free掉后,回挂到free list里。
- Pool 的alloc指针来指向 buffer的来源,来源是由Allocator函数来定义好的
- net_buf 里的 __buf 指针指向分配来的数据块的地址,这块buffer是真正给net_buf的用户用的。
招聘评估方法论
我本来对AI有挺大的偏见,总觉得固定化公式生出来的东西很不可靠。但是我现在觉得我应该好好的研究一下这个东西了。
下面这个文章是用EverNote的AI写的,我给的关键词是 “丹尼尔 卡尼曼” 和 “招聘”。觉得AI总结的丹爷关于招聘的见解很到位,丹爷是帮助军队招聘的时候自己总结出来的关于招聘的想法。
丹尼尔·卡尼曼(Daniel Kahneman),作为行为经济学的奠基人之一,以及诺贝尔经济学奖的得主,他的研究和理论对于招聘领域有着深远的影响。尽管他并没有直接撰写关于招聘的专著,但他在行为经济学领域的贡献,特别是对人类决策过程中的偏差和噪声的理解,为招聘实践提供了宝贵的洞见。
在招聘过程中,卡尼曼的以下观点和指导原则尤为重要:
- 认识到直觉和偏见的局限性
卡尼曼在《思考,快与慢》中提出了“系统1”和“系统2”的思考模式。在招聘中,面试官往往依赖“系统1”的直觉和快速判断,但这种方式容易受到偏见的影响,如首因效应(第一印象)、刻板印象等。因此,他建议招聘者应当意识到这些偏见的存在,并尽量通过“系统2”的慢思考来审视和纠正这些偏见。
- 结构化面试的重要性
卡尼曼在《噪声》一书中揭示了标准面试中存在的巨大噪声,即面试官之间判断的不一致性。他提出,结构化面试是减少这种噪声的有效手段。结构化面试通过设定固定的流程和评价标准,确保每位面试官都能从相同的维度对应聘者进行评估,从而提高招聘的公平性和准确性。
- 分解评估维度
谷歌在招聘中采用了卡尼曼提出的结构化判断原则,即将复杂的评估过程分解为多个具体的维度。例如,人事决策可以分解为一般性认知能力、领导力、文化契合度和角色相关知识等维度。这种分解有助于面试官更清晰地了解每个评估维度的要求,并据此做出更准确的判断。
- 独立评估和推迟整体性判断
为了避免“信息级联效应”和过早给出整体性判断,卡尼曼建议面试官在相互交流之前对候选人进行独立打分。这有助于减少面试官之间的相互影响,确保每位面试官都能根据自己的观察和评估做出独立的判断。同时,他还强调要推迟整体性判断的时间,以免受到有限信息和直觉的干扰。
- 培训和反馈
卡尼曼的理论还强调了培训和反馈在招聘中的重要性。通过培训,面试官可以学习如何识别并纠正自己的偏见和直觉错误;通过反馈,他们可以了解自己的评估结果与团队整体评估结果的差异,并据此进行调整和改进。
综上所述,丹尼尔·卡尼曼的理论为招聘实践提供了宝贵的指导。通过认识偏见的局限性、采用结构化面试、分解评估维度、独立评估和推迟整体性判断以及加强培训和反馈等措施,企业可以更加科学、公平和准确地选拔出合适的人才。
当然,我们可以继续探讨丹尼尔·卡尼曼的理论如何进一步影响和优化招聘流程。
关注候选人的实际行为而非仅仅依赖简历
在卡尼曼的理论中,他强调了人们往往依赖于不完整的或带有偏见的信息来做出决策。在招聘中,这通常表现为过分依赖候选人的简历或面试时的言辞,而忽视了他们的实际行为和表现。因此,招聘者应该尝试通过工作样本测试、案例分析或模拟场景等方式,来观察候选人在实际工作中的表现,以更全面地评估他们的能力和潜力。
建立多元化的评估团队
为了减少偏见和噪声,招聘者可以组建一个多元化的评估团队。这个团队应该包括来自不同背景、经验和专业领域的成员,以便能够从多个角度审视候选人。多元化的团队不仅能提高评估的全面性和准确性,还能促进团队成员之间的交流和碰撞,从而产生更多创新的想法和解决方案。
关注候选人的长期潜力
卡尼曼的理论还提醒我们,人们在做决策时往往过于关注眼前的利益或结果,而忽视了长远的未来。在招聘中,这表现为过分关注候选人的当前能力或经验,而忽视了他们的学习能力、适应能力和发展潜力。因此,招聘者应该关注候选人的长期潜力,考虑他们是否具备适应未来工作环境和变化的能力,以及他们是否有持续学习和成长的动力和意愿。
利用大数据和人工智能技术
随着大数据和人工智能技术的发展,招聘者可以利用这些工具来优化招聘流程和提高招聘效率。例如,通过数据分析来识别招聘过程中的偏见和噪声来源,并制定相应的改进措施;通过人工智能技术来筛选简历和进行初步面试评估等。这些技术可以辅助招聘者做出更科学、更准确的决策,但也需要谨慎使用以避免引入新的偏见或问题。
保持积极和开放的心态
最后但同样重要的是,招聘者应该保持积极和开放的心态来面对招聘过程中的各种挑战和不确定性。招聘是一个双向选择的过程,需要招聘者和候选人之间建立信任和共识。因此,招聘者应该尊重每位候选人的个性和差异,以平等和包容的态度来对待他们。同时,他们也应该保持对新技术和新方法的敏感度和开放度,不断探索和优化招聘流程以适应时代的变化和发展。
解锁情绪大脑
作为公司的基层管理人员,对同仁的的心理健康进行管理也是我们的职责之所在,在他们遇到焦虑,抑郁时希望我能帮他们疏导一下,如果真能让他们能抛开心理包袱开开心心的工作,那真是莫大的欣慰。在同仁有意或是无意地倾诉心理上的烦恼时,我们应该如何安抚他们呢?
- 一种做法叫做conteractivve(反作用),否定他的负面情绪,告诉是他想多了,不应该苦恼,不应该焦虑,哥当年也遇到过,哥是怎么怎么挺过来的,协助他压制他的负面情绪,能起一定的作用,但这种做法无法根除问题。一般地过不了多久问题就又出来了。
- 另外一种就是顺着他说,有情绪是对的,然后跟他一起分析原因,找出时什么导致了现在的问题或烦恼,然后加以安抚。这种方法的效果不稳定,有时有用,有时没用,因为很多时候当事人自己也搞不清楚到底是那件事情触发了他的情绪,所以非常难找到,简直就是靠运气,是玄学。
《Unlock the Emotional Brain》这本书针对第二种做法,总结了一套实用的方法来稳定的找出当事人心理问题的根因,解决了这个问题。
这套方法有如下几个步骤:
A. 理清症状。探究当事人的症状到底是为了解决什么问题,是他在害怕或是逃避什么?比如前文提到的肥胖症患者之所以变肥胖是因为她认为自己变胖了丑了,坏人就不会对她有非分之想了,她自己就安全了,这个原因真是让人意想不到。这些原因往往隐藏的非常之深,肯跟要很多轮谈话才能弄清楚。这些原因可能是一次强烈的情感刺激,幼儿时期的情感依赖等,可能会隐藏的非常深,当事人根本不知道跟当前的症状有任何的关系,所以聊天技巧以及与当事人的信任关系就非常的重要。
B. 将这个症状的根因提升到意识层面,在生活中遇到症状的时候自己能意识到是什么问题,维持现状,但不强迫压制自己的情绪。比如再想狂吃的时候,就要马上意识到自己为什么狂吃,而且要把这个心理活动,很直白地写到小卡片上经常读一读。
C. 与当事人继续聊天,寻找一个反例,最好是亲身经历的真实的事件,而且事件的结果与当前的症状害怕的结果是相反的冲突的,也就是说当担心的某件事情发生的时候,并没有引起当事人担心的事情发生。比如另外一个例子中,Richard在工作中总是不愿当众讲出自己的看法,导致他非常的焦虑。他担心他这么做了,别人就觉得他自大自负,像他的父亲那样。但医生与Richad一起聊出一个反例:另外一个同事在会议中当众讲出了Richard想讲又不敢讲出的观点的时候,其他同事并没有觉得他自负,反而很赞赏他的观点,最后都支持他的观点。这让Richard非常惊讶。
- 引导当事人回想C中找到的反例的当时的场景,激活记忆。
- 回想C中提到的反例。
- 不断的对比当事人的症状和这个反例,让它们在当时事人大脑中不断荡漾。这个步骤是清除大脑中不适记忆的关键步骤。
- V. 验证症状是否消除。 重新聊起症状,看当事人的反应,如果他能想的明白,肯定觉得自己对某些事情过度反应,自己都觉得莫名其妙没有必要。
其中B 和 C需要不断的重复,有可能会有很多个原因需要一个一个的消除。
当然这个步骤对于由于物理创伤或是基因遗传导致的心理症状应该是没啥帮助的。
狂吃症
“每把剃须刀都有它的哲学”,我们总是很快地从表象上去理解身边遇到的每件事,思考不够深入,大多数时候是因为自己带了有色眼镜导致我们自大,自以为是。这个有色眼镜也是我们的大脑思维的牢笼,是非常难破的。
关于情绪大脑的运作,《Unlock the Emotional Brain》这本书真让我爱不释手,介绍了不少非常有意思的案例,受益匪浅。
今天读到一个案例挺让人意外的。
Debbie 有60了,她体重有320磅,减肥一直没成功,她决定做胃部的外科手术来解决这个问题。在做手术前她去找心理医生评估下她的精神状况。
医生在了解状况取得她的同意后,请她躺在沙发上闭上眼睛开始想象自己减肥成功后的好处,引导她自己慢慢说出“如果我减肥成功了,那xxx”,于是她就开始说
“如果我减肥成功了,那我就可以到处走走”
“如果我减肥成功了,那我的狗狗就高兴了,可以跟着我到处跑”
医生说,继续想还有别的吗?
“如果我减肥成功了,那我就不安全了”
于是她潜意识里的导致她狂吃的不愉快的根因慢慢浮出来了。
原来在她小时候非常可爱和吸引人,在她六岁的时候她的叔叔及其朋友对她有过不好的兴趣,她很苦闷,无意中发现自己狂吃后变胖了,这些恶心的人就对她没兴趣了,所以她就一直狂吃到现在这个年纪,找过好多医生,都没有找到这个点。于是医生给她写了一个卡片,让她每天读下面的卡片,随时审视这个从潜意识里浮现出来的心理状态,直到习以为常。
“我不想减肥”
“如果我变瘦了,男人会注意到我,那就会危险”
“所以我必须尽可能保持肥胖和丑陋,那样就没人打扰我了”
“减肥也许可以帮到我的膝盖和背部疼痛”
“但对我来说,保持肥胖和安全更重要”
自此她真的开始减掉了不少磅的体重。
很多的心理问题实际上是个体真实的需求,他需要这个症状来避免他害怕的麻烦。
数字的谬误
平日我们经常在工作中用数字来支持自己的结论,经常因为不够严谨而漏洞百出。在《科学推理》这本书在针对统计推理的介绍中总结了数据应用的谬误,希望自己能时刻注意。
平均数谬误
三个统计学家去打猎,碰上一头大鹿,第一个人开火,结果偏左一米,第二个人开火结果偏右一米。第三个人放下枪欢呼胜利“平均而言我们大众了!”,事实上平均打中了,但一点意义都没有。
《黑天鹅》里塔勒布把世界分为两种:一种叫平均斯坦,一种叫极端斯坦。在极端斯坦的世界里讲平均数就是个笑话,比如包含比尔盖兹的一群人里计算个人财富,得出大家平均财富超高的结论是没有意义的。
绝对数字陷阱
某校今年本科生上线达500人,比去年多了50人,所以喜获丰收
绝对数字要跟相对数字结合才比较有说服力。
百分比陷阱
肺结核发病正在迅速增长,今年肺结核数量增长的比率是去年的4倍。
比率是四倍不意味着人数是四倍。
赌徒谬误
一对农村夫妇,特别想要一个男孩,生了第一个孩子是女儿,叫招弟;第二个是女儿,叫跟弟;第三个还是女儿叫听弟;第四个还是女儿叫等弟;第五个到第七个还是女儿,分别叫候弟,盼弟,望弟;一个七个女儿就是没有男孩。其实不管生几个女儿,下一个生儿女的概率各是50%。
数字和结论不相关
近十年里来,德克萨市州博士的数量每年增加5.5%,而该州骡子的数量每年减少5.5%,所以博士数量额增长导致了骡子数量的下降。
在《因果关系》中将这种关系定义为为叉结构: 博士数量增长 <— 城市化进程 —> 骡子的数量下降
数据不可比
今年本公司的汽车销售了10万辆,是竞争对手公司的一倍,我们的好年头来了。
统计表明,大多数医疗事故出在大医院,因此去小医院看病比较安全。
独立数据
某国卫生部门统计,2004年全国糖尿病患者中70%为肥胖者。这说明,肥胖将极大地增加患糖尿病的危险。
实际上很可能全国的肥胖者比率就是70%,跟糖尿病没事哈关系。
Eat and Run
Eat & Run 这本书挺值得一读,牛人们都有一套自己的套路。
作者Scott Jurek是一位70后,拿过10多个超马冠军。书本主要记录了很多自己参加超马时候的内心活动,讲述自己如何一次次超越绝望和痛苦,捧得冠军。
他是个素食主义者,在这本自传中加了很多自己的食谱,他经常把自己收集到的植物性食材塞到料理机里一顿搅和就是一顿佳肴,哈哈,也挺简单的。他认为现代农业带来的所谓文明食物饱含着各种毒素,抗生素等等,对身体摧残非常大,他的素食反而是解决这个问题的方法,他是无意中参加滑雪比赛的时候,发现自己吃素食可以让体能恢复的非常快,就走上了这条路。
不同于我这样的佛系跑着,跑超级马拉松(100km 以上)光靠体力绝对是撑不过去的,靠的是跑者的坚强意志,是个人意志在克服百般痛苦后,身体进入一种“心流”,无我和空灵状态的爽朗。
几个金句:
理性的评估带来的结果很可能就是理性的放弃。
有时候后理性反而是成事最大的绊脚石。
我们在绵延的路上奔跑,跑进狭窄的小径,荒芜的小道,撞见林间跃动的小鹿,漫步的土狼。我们踏过积雪及膝的路面,横渡春雪初融的溪水,双腿因寒冷而麻木。我硬着头皮撑下去,大口喘着气,跑过亚多夫超市。
如今跑步不再是单纯的训练,而已变成我沉淀思绪的方式。
跑步时,我的心灵尽情奔放,脱离尘嚣,不再为学业,未来与母亲的病情烦恼。我的身体任凭意志摆布。我不再把自己锁在没有出口的死路上,再也没有坏学生吵我脸上吐口水。我仿佛摆脱重力,一跃登上天梯。
关于跑步心得:
用腹式呼吸,平躺在床上,肚皮上放一本书,体会在呼吸的时候书本跟着上下起伏的感觉。
还有人提到平时要多拉升横膈膜,也有助于腹式呼吸的深度。用鼻子呼吸,只有用鼻子呼吸,才对大脑有益。
我试了下,单纯只用鼻子呼吸,心跳也比较容易稳定,不容易飙的很高。这也有个好处是可以温暖下空气和鼻腔,我试过在冬天鼻子只用来吸气,嘴巴呼气,鼻子特难受,跑完之后得连续打10多个喷嚏。