作者信息
本文主要分析了 Apache NimBLE v1.5 版本的 BLE HCI 层设计,并分析了官方仓库自带 UART 对接例程;关于 BLE 层次结构可以先看一下这篇参考文档。
NimBLE 目录结构
1NimBLE
2 ├───apps /* Bluetooth 示例应用程序 */
3 ├───docs /* 官方文档及 API 说明 */
4 ├───ext
5 ├───nimble
6 │ ├───controller /* Controller 实现 */
7 │ ├───doc /* 当前包含 transport 层说明文档 */
8 │ ├───drivers /* Nordic 系列 Phy 驱动 */
9 │ ├───host /* Host Stack(主机控制器)实现 */
10 │ ├───include
11 │ ├───src
12 │ └───transport /* HCI 传输抽象层 */
13 └───porting /* OS 抽象层及系统配置 */
观察目录,可以看出 NimBLE 是实现了 Host 与 Controller 分离的,在官方介绍中也有说明。
nimble 的 Host 可以跑在任一芯片上;Controller 则限制比较多,需要有特殊的硬件以及驱动支持。
nimble/controller 则是 Controller 相关代码;nimble/host 对应 Host 代码;nimble/transport 就是 HCI 层代码;这一版本 nimble/doc 下还包含了 HCI 层的说明文档 transport.md。
顺带一提,porting 目录下是 OS 抽象层及系统配置;rt-thread 的移植就实现在 porting/npl 下。
NimBLE HCI 层
主机控制接口层(Host Controller Interface,简写 HCI):HCI是可选的,主要用于2颗芯片实现BLE协议栈的场合(一个当作 Host 一个当作 Controller),用来规范两者之间的通信协议和通信命令等。
NimBLE HCI 层主要是了解 nimble/transport 下的内容。首先看一下官方文档 nimble/doc/transport.md 中对 transport 层的说明,主要看下面这张图:
HCI 层包括这4接口:
Host 从 Controller 端接收接口 ble_transport_to_hs_evt 和 ble_transport_to_hs_acl;以及 Host 向 Controller 发送接口 ble_transport_to_ll_cmd 和 ble_transport_to_ll_acl。
在目录下,官方流出的接口定义主要包含在下面几个文件中:
1transport
2 └───inlucde
3 ├───nimble
4 │ └───transport
5 │ └───monitor.h
6 ├───transport_impl.h
7 └───transport.h
其中比较重要的是 transport_impl.h 文件,从名字也可以看出这是一个需要实现的接口定义文件,其主要内容如下:
1/* Init functions to be implemented for transport acting as HS/LL side */
2extern void ble_transport_ll_init(void);
3extern void ble_transport_hs_init(void);
4/* APIs to be implemented by HS/LL side of transports */
5extern int ble_transport_to_ll_cmd_impl(void *buf);
6extern int ble_transport_to_ll_acl_impl(struct os_mbuf *om);
7extern int ble_transport_to_hs_evt_impl(void *buf);
8extern int ble_transport_to_hs_acl_impl(struct os_mbuf *om);
这些接口在 transportinclude imble ransportmonitor.h 中被引用:
1static inline int
2ble_transport_to_ll_cmd(void *buf)
3{
4 return ble_transport_to_ll_cmd_impl(buf);
5}
6static inline int
7ble_transport_to_ll_acl(struct os_mbuf *om)
8{
9 return ble_transport_to_ll_acl_impl(om);
10}
11static inline int
12ble_transport_to_hs_evt(void *buf)
13{
14 return ble_transport_to_hs_evt_impl(buf);
15}
16static inline int
17ble_transport_to_hs_acl(struct os_mbuf *om)
18{
19 return ble_transport_to_hs_acl_impl(om);
20}
看完这个文件,大概明白了,之前官方文档图中提到的 HCI 4个接口,在这里与对应的 _impl() 接口绑定了起来。而 impl() 接口就是官方提供给开发者具体实现对接的接口。
由于 Host 和 Contoller 是双向交互的,所以发送与接收 HCI 包接口是要完整实现的,也就是大多数情况下上述 4 个 *impl() 接口都需要全部实现。
分析 UART 对接 HCI 层官方例程
官方提供了一个使用 UART 对接 HCI 层的例程,源码文件为
nimble/transport/uart/src/hci_uart.c
首先找到熟悉的接口:代码中显式实现了 ble_transport_to_hs_evt_impl 以及 ble_transport_to_hs_acl_impl ,这两个接口中基本上就是使用 uart 向 host 发送数据包,涉及到某个板子 uart 发送数据的具体细节,这里不过多关注。
看完上面两个接口,其实 HCI 中向 Host 发送数据包功能算实现完了,且当前文件中没有实现其他的 impl 接口,很容易能想到,这是写的 Controller 端的 HCI 层代码。
有了这个定性信息,可以开始分析如何从 uart 中接收 Host 层发来的数据包。
通过 uart 初始化函数找到 uart 接收回调函数:
1// uart 初始化函数
2rc = hal_uart_init_cbs(MYNEWT_VAL(BLE_TRANSPORT_UART_PORT),
3 hci_uart_tx_char, NULL,
4 hci_uart_rx_char, NULL);
1// uart 接收回调函数
2static int hci_uart_rx_char(void *arg, uint8_t data)
3{
4 hci_h4_sm_rx(&hci_uart_h4sm, &data, 1);
5 return 0;
6}
可以看到每接收一个字符,都使用 hci_h4_sm_rx 进行接收。该函数声明在 transportcommonhci_h4include imble ransporthci_h4.h 文件下,是关于 H4 的一个函数,大概看一下具体定义,接收字符后有一个组帧判断的过程。看一下 hci_h4.h 下比较关键的两个接口:
1void hci_h4_sm_init(struct hci_h4_sm *h4sm,
2 const struct hci_h4_allocators *allocs,
3 hci_h4_frame_cb *frame_cb);
4int hci_h4_sm_rx(struct hci_h4_sm *h4sm, const uint8_t *buf, uint16_t len);
hci_h4_sm_init() 中出现了一个 hci_h4_frame_cb *frame_cb 参数,这是一个函数指针参数,初步猜测用于回调函数的注册。且在
hci_uart.c 代码中,也找到了 hci_h4_sm_init() 相关调用:
244: hci_h4_sm_init(&hci_uart_h4sm, &hci_h4_allocs_from_hs, hci_uart_frame_cb)
这里将 hci_uart_frame_cb 注册成了回调函数,源码中定义如下:
1static int
2hci_uart_frame_cb(uint8_t pkt_type, void *data)
3{
4 switch (pkt_type) {
5 case HCI_H4_CMD:
6 return ble_transport_to_ll_cmd(data);
7 case HCI_H4_ACL:
8 return ble_transport_to_ll_acl(data);
9 default:
10 assert(0);
11 break;
12 }
13 return -1;
14}
hci_uart_frame_cb 基本上是对一个完整的 HCI 包的处理,根据不同的类型使用 ble_transport_to_ll_cmd 和 ble_transport_to_ll_acl 传输给 Link Layer 层( Link Layer 是 Controller 上的一个层次,更加确定这是 Controller 上的 HCI 层实现 )。
结合 hci_uart.c 中对 hci_h4_sm 的使用,以及对 hci_h4_sm 相关接口源码的分析,hci_h4_sm 其实是官方提供的一个类似保证包完整性的东西,用于判断一帧完整的 HCI 数据包,并且提供组包完成回调函数的机制。
看到这里,大概脉络应该已经捋清楚了。这是一个 Controller 上的 UART HCI 层对接实现:
1、向 Host 发送 HCI 包:主要通过显式实现 ble_transport_to_hs_evt_impl 以及 ble_transport_to_hs_acl_impl 接口实现,具体何时被调用,协议栈已经自动处理好。
2、从 Host 接收 HCI 包:主要是使用的 hci_h4 中的组包接口,hci_h4_sm 即一个组包的状态机实例,通过 hci_h4_sm_rx 接收 uart 接收到的字符,在判断 hci 包完整接收时调用提前注册好的 回调函数 hci_h4_frame_cb 。hci_h4_frame_cb 里则实现了将 uart 接收到的包传递给 LL 层,进而 Controller 可以对 Host 传下来的命令或数据做出响应动作。
完整的 HCI 层实现
Controller 使用 UART 做 HCI 层数据传输
开始说到无论在什么情况下都要完成 HCI 层中 4个主要接口的实现,当前 hci_uart.c 中只找到了两个接口的实现,还有另外两个接口在哪呢。使用全局搜索在 nimblecontrollersrcle_ll.c 下找到了另外两个接口的实现:
1/* Transport APIs for LL side */
2int
3ble_transport_to_ll_cmd_impl(void *buf)
4{
5 return ble_ll_hci_cmd_rx(buf, NULL);
6}
7int
8ble_transport_to_ll_acl_impl(struct os_mbuf *om)
9{
10 return ble_ll_hci_acl_rx(om, NULL);
11}
因为目前默认在 Controller 端实现 HCI 层,剩下未实现的两个接口在 Controller 源码下实现了。
Host 使用 UART 做 HCI 层数据传输
那么假设当前需要实现 Host 端的 HCI 层,且还是需要使用 UART 对接。那么需要实现对接 UART 的接口应该刚好和 hci_uart.c 中相反。
需要在对接源码中,实现 ble_transport_to_ll_cmd_impl 和 ble_transport_to_ll_acl_impl 发送接口,具体同样也是使用 UART 发送包。相应的,在 nimblehostsrcle_hs.c 下找到了以下两个接口的实现:
1/* Transport APIs for HS side */
2int
3ble_transport_to_hs_evt_impl(void *buf)
4{
5 return ble_hs_hci_rx_evt(buf, NULL);
6}
7int
8ble_transport_to_hs_acl_impl(struct os_mbuf *om)
9{
10 return ble_hs_rx_data(om, NULL);
11}
也就实现了 Host端 HCI 层的完整搭建。
在使用 UART 对接 HCI 层时,是默认当前环境只跑了 Host 或 Controller 中的一端,因此 nimblecontrollersrcle_ll.c 和nimblehostsrcle_hs.c 不会同时参与编译,自然也不会引起一些 *impl() 接口重复定义的错误。
不需要其他接口进行 HCI 传输
当 Host 与 Controller 跑在同一环境中,不需要对接具体的数据传输接口, nimblecontrollersrcle_ll.c 和nimblehostsrcle_hs.c 同时参与编译,此时无需再编写额外的代码,即可完整实现整个 HCI 层。
总结
在较早的版本中,NimBLE 对接 HCI 层需要实现以下接口(参考文档):
1int ble_hci_trans_hs_cmd_tx(uint8_t *cmd);
2int ble_hci_trans_hs_acl_tx(struct os_mbuf *om);
3void ble_hci_trans_cfg_hs(ble_hci_trans_rx_cmd_fn *cmd_cb,
4 void *cmd_arg,
5 ble_hci_trans_rx_acl_fn *acl_cb,
6 void *acl_arg);
7uint8_t *ble_hci_trans_buf_alloc(int type);
8void ble_hci_trans_buf_free(uint8_t *buf);
9int ble_hci_trans_set_acl_free_cb(os_mempool_put_fn *cb, void *arg);
10int ble_hci_trans_reset(void);
可以看出来有一些麻烦,除了处理数据传输方向,还有处理数据包的内存申请与释放,实现起来较为复杂。
当前版本中官方对 HCI 层进行了重构,在 transport.c 中统一处理了数据包的内存申请与释放;让开发者专注于处理数据的发送与接收方式,并且提供了 H4 类型的 HCI 包接收工具,更加方便开发者的对接使用。
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !