09-對part7-bluetooth的翻譯和搬運
所有非黑色的字體均為我自己添加
原文放在文章末尾
啟動藍牙
????????僅僅用一個UART連接的電腦來控制樹莓派并不是很有趣.我們的Breakout游戲值得比它更好的控制器 -- 最好是無線的.
????????在這一部分中,我們設(shè)置了第二個UART來和樹莓派板上的藍牙調(diào)制調(diào)節(jié)器(modem)進行溝通.關(guān)于藍牙的東西并不簡單,但至少比USB簡單.這就是我選擇它的原因.
博通的固件
????????藍牙調(diào)制調(diào)節(jié)器是一款博通公司的芯片(BCM43455),并且它在工作前需要加載專有的軟件.我已經(jīng)從藍牙倉庫(https://github.com/RPi-Distro/bluez-firmware/tree/master/broadcom)中取得了它們的 BCM4345C0.hcd 文件.
????????由于我們并沒有任何文件系統(tǒng),所以我們不能在運行時加載它,而是把它編譯到我們的內(nèi)核里.我們可以使用 objcopy來編譯一個可以鏈接的 a.o 文件.我們在Makefile中添加這些行:
BCM4345C0.o : BCM4345C0.hcd
$(GCCPATH)/aarch64-none-elf-objcopy -I binary -O elf64-littleaarch64 -B aarch64 $< $@
????????我們也需要修改我們的 kernel8.img 依賴來包含新的 .o 文件
kernel8.img: boot.o $(OFILES) BCM4345C0.o
$(GCCPATH)/aarch64-none-elf-ld -nostdlib -nostartfiles boot.o $(OFILES) BCM4345C0.o -T link.ld -o kernel8.elf
$(GCCPATH)/aarch64-none-elf-objcopy -O binary kernel8.elf kernel8.img
????????如果你現(xiàn)在編譯內(nèi)核,你就會發(fā)現(xiàn)鏡像文件變得非常大 -- 因為它包含了固件.你可以運行?objdump -x kernel8.elf 然后你可以在這里看見一些新的符號:
_binary_BCM4345C0_hcd_start
_binary_BCM4345C0_hcd_size
_binary_BCM4345C0_hcd_end
????????我們稍后將使用這些符號來引用C代碼中的固件。
設(shè)置UART
????????看到我們的新 bt.c .現(xiàn)在我們將要關(guān)注 //UART0 這一節(jié).大多數(shù)技術(shù)對你來講都會很熟悉,因為這個硬件也是使用MMIO,并且我們要實現(xiàn)我們實現(xiàn)過的函數(shù)來進行一系列的debug工作.
????????為了在我們debug的同時使用藍牙,我們將要重新映射GPIO引腳.30,31,32,33腳需要選擇函數(shù)3來讓我們訪問CTS0, RTS0, TXD0 和 RXD0.你可以在?BCM2711 ARM 外設(shè)手冊(https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf)中讀到關(guān)于它的一切.
????????我們已經(jīng)有了 io.c 中的函數(shù)來做這些,所以我們把 gpio_useAsAlt3 的函數(shù)定義添加到 io.h 中來讓我們從這里可以訪問它.在 bt_init() 中,我們繼續(xù),并且重新映射GPIO引腳,然后沖刷接收緩沖區(qū)保證安全.在它后面寫的MMIO代碼,設(shè)置了115200的波特率和8-N-1的通訊(這里是1位起始位,8位數(shù)據(jù)位,沒有奇偶校驗的簡寫) -- 我們知道藍牙調(diào)制調(diào)解器可以處理這些.可能最重要的要搞對的是這個:
mmio_write(ARM_UART0_CR, 0xB01);
????????它使能了串口(bit 0?置1),使能 TX和RX (bit 8 和 9 置1),并且把 RTS0 拉低(bit 11 置1) -- 非常重要,因為藍牙調(diào)制調(diào)解器不會應(yīng)答,除非它看見這個.我花了很多時間來解決這個問題.
????????我們可以使用UART來和藍牙調(diào)制調(diào)解器通信了.
與藍牙調(diào)制調(diào)解器通信
????????藍牙規(guī)范是很繁重的,實現(xiàn)一個完全的驅(qū)動需要花上好些時間.我現(xiàn)在就要解決"證明生活".我們?nèi)匀挥蟹椒ㄍㄟ^...(這句的翻譯拿不準,原文為 I'll settle for "proof of life" for now. We still have a way to travel though...)
????????我們將要使用 HCI命令同藍牙調(diào)制調(diào)解器通信.作為這個的介紹,我享受閱讀TI HCI 文檔(https://software-dl.ti.com/simplelink/esd/simplelink_cc13x2_sdk/1.60.00.29_new/exports/docs/ble5stack/vendor_specific_guide/BLE_Vendor_Specific_HCI_Guide/hci_interface.html)
????????bt_reset()只是調(diào)用 hci_Command,它又調(diào)用 hci_CommandBytes 來向UART傳輸字符從而告訴藍牙芯片重置并且等待固件.這是一個和供應(yīng)商有關(guān)的調(diào)用,所以你不會在任何地方找到文檔.我使用樹莓派Linux發(fā)行版中的以下文件對調(diào)用進行了反向工程:
https://github.com/raspberrypi/linux/blob/rpi-5.10.y/drivers/bluetooth/btbcm.c
https://github.com/raspberrypi/linux/blob/rpi-5.10.y/drivers/bluetooth/hci_bcm.c
https://github.com/raspberrypi/linux/blob/rpi-5.10.y/include/net/bluetooth/hci.h
????????hci_CommandBytes 之后在它返回成功前等待一個非常特殊的應(yīng)答 -- "command complete" 應(yīng)答.
加載固件
????????現(xiàn)在設(shè)備在等待.我們要向它發(fā)送我們內(nèi)核中的固件字節(jié).
void bt_loadfirmware()
{
? ? volatile unsigned char empty[] = {};
? ? if (hciCommand(OGF_VENDOR, COMMAND_LOAD_FIRMWARE, empty, 0)) uart_writeText("loadFirmware() failed\n");
? ? extern unsigned char _binary_BCM4345C0_hcd_start[];
? ? extern unsigned char _binary_BCM4345C0_hcd_size[];
? ? unsigned int c=0;
? ? unsigned int size = (long)&_binary_BCM4345C0_hcd_size;
? ? unsigned char opcodebytes[2];
? ? unsigned char length;
? ? unsigned char *data = &(_binary_BCM4345C0_hcd_start[0]);
? ? while (c < size) {
? ? ? ? opcodebytes[0] = *data;
? ? ? ? opcodebytes[1] = *(data+1);
? ? ? ? length =? ? ? ? ?*(data+2);
? ? ? ? data += 3;
? ? ? ? if (hciCommandBytes(opcodebytes, data, length)) {
? ? ? ? ? ?uart_writeText("Firmware data load failed\n");
? ? ? ? ? ?break;
? ? ? ? }
? ? ? ? data += length;
? ? ? ? c += 3 + length;
? ? }
? ? wait_msec(0x100000);
}
????????首先,我們發(fā)送一條指令告訴芯片我們將要發(fā)送固件.你將會看到我們參考我們的新符號,它們指向我們的固件字節(jié).我們現(xiàn)在知道了固件的大小,所以我們可以在其之上迭代.
????????固件只是一系列遵從這樣的格式的HCI命令:
兩字節(jié)操作符
一字節(jié)告訴我們后面跟著的數(shù)據(jù)的長度
相應(yīng)長度字節(jié)的數(shù)據(jù)
????????我們進行的時候要檢查每個命令是否成功,等待1秒然后返回.如果它正確運行,我們就加載好了固件,可以準備藍牙通訊了.
????????我選擇實現(xiàn)了 bd_setbaud() 和 bt_setbdaddr(). 這個設(shè)置了藍牙調(diào)制調(diào)解器的通話速率(就像我們在串口的例子中所做的那樣),和其獨特的藍牙設(shè)備地址(https://macaddresschanger.com/what-is-bluetooth-address-BD_ADDR)
建造一座埃迪斯通燈塔
????????可能可以建造的最簡單的藍牙設(shè)備就是"燈塔".它只是發(fā)布了少量的數(shù)據(jù),任何經(jīng)過的接收者都能看到這些數(shù)據(jù).一個典型的用法就是為了基于位置的視場營銷目的而發(fā)布的網(wǎng)絡(luò)URL.
? ? ? ? 谷歌定義了埃迪斯通格式(https://en.wikipedia.org/wiki/Eddystone_(Google)),它被廣泛的采用.我們將要在這個例子里面實現(xiàn)它.這是我們需要完成的:
設(shè)置LE事件掩膜來確保藍牙控制器可以被所有到來的事件中斷
設(shè)置廣播參數(shù)
設(shè)置廣播數(shù)據(jù)
啟動廣播
????????我的大部分學習都是由markfirmware的代碼(https://github.com/markfirmware/zig-bare-metal-raspberry-pi/blob/master/src/ble.zig)引導(dǎo)的.我著重閱讀了相關(guān)藍牙標準( https://www.bluetooth.com/specifications/specs/core-specification-5-2/)的章節(jié),而不是盲目的把代碼粘貼到我的代碼中.我推薦你也這么做,如果你同樣對這里如何運行感興趣的話.
????????當構(gòu)建廣播數(shù)據(jù)的時候,我推薦閱讀PiMyLifeUp的文章(https://pimylifeup.com/raspberry-pi-eddystone-beacon/).
????????為了測試代碼,確保 run_eddystone() 在內(nèi)核中沒有被注釋掉,而不是 run_search() (我們將要在 part8-breakout-ble中對run_search() 做更詳細的探討)
????????構(gòu)建并運行后,我使用eBeacon iPhone應(yīng)用程序檢查我的Eddystone信標是否在廣播。下面的截圖顯示了我發(fā)布的URL:

????????

Getting Bluetooth up
Controlling the RPi4 solely via a UART-connected laptop is not much fun. Our Breakout game deserves a better controller than that - ideally a wireless one.
In this part, we set up a second UART to communicate with the RPi4's onboard Bluetooth modem. There is nothing simple about Bluetooth, but it is at least simpler than USB, and that's the reason I've chosen to pursue it.
The Broadcom firmware
The Bluetooth modem is a Broadcom chip (BCM43455), and it needs to be loaded with proprietary software before it's useful to us. I have taken their _BCM4345C0.hcd_ file from the [Bluez repo](https://github.com/RPi-Distro/bluez-firmware/tree/master/broadcom).
As we don't yet have any filesystem, we won't be able to load this at runtime, so instead we'll need to build it into our kernel. We can use `objcopy` to build a _.o_ file that we can link. We add these lines to _Makefile_:
```c
BCM4345C0.o : BCM4345C0.hcd
$(GCCPATH)/aarch64-none-elf-objcopy -I binary -O elf64-littleaarch64 -B aarch64 $< $@
```
We also need to modify our `kernel8.img` dependencies to include our new _.o_ file:
```c
kernel8.img: boot.o $(OFILES) BCM4345C0.o
$(GCCPATH)/aarch64-none-elf-ld -nostdlib -nostartfiles boot.o $(OFILES) BCM4345C0.o -T link.ld -o kernel8.elf
$(GCCPATH)/aarch64-none-elf-objcopy -O binary kernel8.elf kernel8.img
```
If you build the kernel now, you'll see that the new image is much bigger - because it contains the firmware bytes. You can run `objdump -x kernel8.elf`, and you'll also see a few new symbols in there:
?* _binary_BCM4345C0_hcd_start
?* _binary_BCM4345C0_hcd_size
?* _binary_BCM4345C0_hcd_end
We'll use these symbols later to reference the firmware from our C code.
Setting up the UART
Look at our new _bt.c_. We'll focus on the `// UART0` section for now. A lot of the techniques will be familiar to you as this hardware also uses the MMIO technique, and we're implementing a lot of the same functions that we did to get our serial debug working.
To use Bluetooth at the same time as our serial debug, we'll need to remap some GPIO pins. Pins 30, 31, 32 and 33 will all need to take on their _alternate function 3_ to give us access to CTS0, RTS0, TXD0 and RXD0.? You can read all about this in Section 5.3 of the [BCM2711 ARM Peripherals document](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711/rpi_DATA_2711_1p0.pdf).
We already have a function in _io.c_ to do this, and so we add the function definition of `gpio_useAsAlt3` to _io.h_ to give us access to it from here. In `bt_init()`, we go ahead and remap the GPIO pins, then flush the receive buffer just to be safe. The MMIO writes which follow, set us up for 115200 baud and [8-N-1 communication](https://en.wikipedia.org/wiki/8-N-1) - we know the Bluetooth modem can cope with this. Perhaps the most important line to get right was this one:
```c
mmio_write(ARM_UART0_CR, 0xB01);
```
It enables the UART (bit 0 on), enables TX & RX (bits 8 & 9 on), and drives RTS0 low (bit 11 on) - very important as the Bluetooth modem will sit unresponsive until it sees this. **I lost a lot of time figuring this one out.**
We should now be able to talk to the Bluetooth modem over our new UART.
Talking to the Bluetooth modem
The Bluetooth spec is massive, and to implement a full driver would take a long while. I'll settle for "proof of life" for now. We still have a way to travel though...
We talk to the Bluetooth modem using **HCI commands**. I enjoyed reading the [TI HCI docs](http://software-dl.ti.com/simplelink/esd/simplelink_cc13x2_sdk/1.60.00.29_new/exports/docs/ble5stack/vendor_specific_guide/BLE_Vendor_Specific_HCI_Guide/hci_interface.html) as an intro to this.
`bt_reset()` simply calls `hci_Command`, which in turn called `hci_CommandBytes` to write the bytes out to the UART that tell the Bluetooth chip to reset and await firmware. This is a vendor-specific call, so you won't find it documented anywhere. I reverse-engineered the calls using the following files from the Raspberry Pi Linux distribution:
* https://github.com/raspberrypi/linux/blob/rpi-5.10.y/drivers/bluetooth/btbcm.c
* https://github.com/raspberrypi/linux/blob/rpi-5.10.y/drivers/bluetooth/hci_bcm.c
* https://github.com/raspberrypi/linux/blob/rpi-5.10.y/include/net/bluetooth/hci.h
`hci_CommandBytes` then waits for a very specific response before it returns successfully - the "command complete" response.
Loading the firmware
Now the device is waiting. We need to send it the firmware bytes we included in our kernel:
```c
void bt_loadfirmware()
{
? ? volatile unsigned char empty[] = {};
? ? if (hciCommand(OGF_VENDOR, COMMAND_LOAD_FIRMWARE, empty, 0)) uart_writeText("loadFirmware() failed\n");
? ? extern unsigned char _binary_BCM4345C0_hcd_start[];
? ? extern unsigned char _binary_BCM4345C0_hcd_size[];
? ? unsigned int c=0;
? ? unsigned int size = (long)&_binary_BCM4345C0_hcd_size;
? ? unsigned char opcodebytes[2];
? ? unsigned char length;
? ? unsigned char *data = &(_binary_BCM4345C0_hcd_start[0]);
? ? while (c < size) {
? ? ? ? opcodebytes[0] = *data;
? ? ? ? opcodebytes[1] = *(data+1);
? ? ? ? length =? ? ? ? ?*(data+2);
? ? ? ? data += 3;
? ? ? ? if (hciCommandBytes(opcodebytes, data, length)) {
? ? ? ? ? ?uart_writeText("Firmware data load failed\n");
? ? ? ? ? ?break;
? ? ? ? }
? ? ? ? data += length;
? ? ? ? c += 3 + length;
? ? }
? ? wait_msec(0x100000);
}
```
First, we send a command to tell the chip that we're about send the firmware. You'll see that we then reference our new symbols, which point us at our firmware bytes. We now know the size of the firmware, and so we iterate over it.
The firmware is simply a sequence of HCI commands following this format:
?* 2 bytes of opcode?
?* 1 byte that tells us the length of the data to follow
?* _length_ bytes of data
We check each HCI command succeeds as we go, wait a second and then return. If it runs without error then we've loaded our firmware and we're ready to start some Bluetooth communications.
I've then chosen to implement `bd_setbaud()` and `bt_setbdaddr()`. This sets the speed at which the Bluetooth modem will talk (much like we did in our UART examples) and also its unique [Bluetooth Device Address](https://macaddresschanger.com/what-is-bluetooth-address-BD_ADDR).
Building an Eddystone beacon
Perhaps the simplest Bluetooth device to build is a "beacon". It simply advertises a small amount of data publicly, such that any passing receivers can view the data. A typical use case is to advertise a web URL for location-based marketing purposes.
Google defined the [Eddystone format](https://en.wikipedia.org/wiki/Eddystone_(Google)), which was reasonably widely adopted. We'll implement this as our example. Here's what we need to achieve:
?* Set the LE event mask to ensure that the Bluetooth controller is interrupted by all incoming traffic
?* Set advertising parameters
?* Set advertising data
?* Enable advertising
Much of my learning was advanced by [markfirmware's code](https://github.com/markfirmware/zig-bare-metal-raspberry-pi/blob/master/src/ble.zig). Rather than just blindly copying code into my _bt.c_, I read this alongside relevant sections of the weighty [Bluetooth specification](https://www.bluetooth.com/specifications/specs/core-specification-5-2/). I recommend you do the same if you're interested in truly understanding what's going on here.
When constructing the advertising data, I also referred to [PiMyLifeUp's article on Eddystone](https://pimylifeup.com/raspberry-pi-eddystone-beacon/).
To test the code, ensure `run_eddystone()` is uncommented in _kernel.c_ instead of `run_search()` (we'll talk about `run_search()` in more detail in part8-breakout-ble).
Once built and running, I used the [eBeacon iPhone application](https://apps.apple.com/us/app/ebeacon-ble-scanner/id730279939) to check that my Eddystone beacon was broadcasting. The screenshots below show my URL proudly advertised as intended: