做BLE模块开发这几年,我最大的感受是:很多人在“选模块还是选芯片”这个问题上纠结很久,最后选了模块,结果发现模块买回来只是省了射频和协议栈的功夫,应用逻辑还是得靠一颗外部MCU来跑。实际上有一种被低估的方案——BLE模块直接支持smartBASIC这类脚本语言,让模块自己把应用跑起来。这篇文章我就以实际开发过的Laird Connectivity(现在归入Ezurio品牌)BL654模块为例,聊聊smartBASIC这条技术路线到底能干什么、适合谁,以及从编写、编译到烧录跑通的完整过程。
1. 先搞懂smartBASIC在解决什么问题:BLE模块开发的两条路线
1.1 传统方案:把BLE模块当成“无线串口管道”
大部分人对BLE模块的认知是这样的:模块就像一个无线串口,MCU通过UART往模块里丢数据,模块再通过BLE协议栈把数据发出去。反过来,手机发过来的数据也由模块转成串口字节流交给MCU处理。这种模块通常叫“透传模块”,很多产品里的做法就是“MCU + AT命令控制BLE模块”。
这种方案最大的优点是开发快、门槛低。你不需要理解GATT、ATT、L2CAP这些协议层细节,只要会用串口发几条AT命令,模块就能连上手机收发数据。我也是从这条路入门的,当时做第一个BLE项目时,大概花了一周就把设备和手机App之间的通信跑通了。
但用久了就会发现它的痛点很明显:
第一,外部MCU省不掉。传感器、按键、LED、显示屏这些外围设备总得有人管,模块本身只是把数据从串口搬到蓝牙上,所有业务逻辑还是得写在外部MCU里。BOM上有两颗芯片,成本和体积都上去了。
第二,功耗很难降到极致。为了跟模块通信,外部MCU就不能长时间深度睡眠,不然串口数据来了没人处理。而BLE最大的卖点本来就是低功耗,结果被外部MCU拖了后腿。
第三,串口是瓶颈。即使模块的BLE吞吐量能跑到几十KB/s,UART的波特率和收发机制也会限制实际数据速率,对于需要传输较大数据量的场景很不友好。
1.2 smartBASIC方案:让模块自己成为“应用大脑”
smartBASIC的思路完全不同:既然模块内部的SoC本身就有完整的处理器核、内存和外设控制器,那为什么不让应用逻辑直接跑在模块上?
smartBASIC是一种基于BASIC语法的脚本语言,由Laird Connectivity为其BLE模块专门设计,早期的BL600系列就在用,后来的BL654也完整支持。你不需要外挂MCU,直接在模块里写脚本控制GPIO、ADC、UART、PWM,同时模块内部已经帮你封装好了BLE协议栈,脚本里一个函数就能注册服务、发广播、回读写请求。
用个不恰当的类比:普通透传模块好比一个对讲机,你按下按钮只能按固定频道说话;smartBASIC模块更像一台带着无线网卡的小电脑,你可以在里面编程,什么时候说话、说什么内容、怎么处理信号都由自己决定。
我实际用下来,这种方案有几个很实际的好处:
- 系统功耗可以做得非常低。模块平时可以进sleep状态,靠GPIO事件、BLE连接事件或者定时器唤醒,应用逻辑和无线协议栈都在一颗芯片里,不需要跨芯片通信,也不会因为外部MCU空转浪费能量。
- BOM精简。少一颗MCU,PCB面积、贴片成本、外围去耦电容都跟着省,对成本敏感的产品帮助明显。
- 响应快。GPIO中断直接在模块内部处理,不需要经过串口转发到外部MCU再绕回来,从事件发生到执行动作的路径短很多。
1.3 两种路线怎么选:一张对照表
我整理过一张选型对照表,基本可以覆盖大多数项目的判断依据:
| 对比维度 | 透传模块 + 外部MCU | smartBASIC模块直接编程 |
|---|---|---|
| 是否需要外部MCU | 一般需要 | 不需要,模块本身可编程 |
| 系统功耗 | 较高,外部MCU难长时间睡眠 | 低,模块可深度睡眠,事件唤醒 |
| 开发门槛 | 低,AT命令即可上手 | 中等,需要学smartBASIC语法和API |
| 灵活性 | 受限,通常只能做透传或简单协议 | 高,可自定义外设逻辑和BLE服务 |
| 数据吞吐 | 受串口速率限制 | 模块内部直接访问协议栈,路径更短 |
| BOM成本 | 两颗主芯片加外围 | 单颗模块 |
| 适用产品 | 快速验证、简单透传、已有MCU架构 | 低功耗传感器、可穿戴、小批量定制设备 |
需要注意的是,这两种路线并不总是非此即彼。有些模块同时支持AT命令和smartBASIC,你可以先在AT模式下快速验证硬件,再切换到smartBASIC做深入开发。BL654就是这么个路子,硬件上是同一颗nRF52840,只是固件和运行环境不同。
2. smartBASIC的核心机制:事件驱动、GATT封装和API体系
2.1 为什么BLE应用天然适合事件驱动
刚开始写嵌入式固件的人,习惯的编程模型是轮询:一个while(1)循环,不断读按键、查标志位、处理数据。这种模型在简单场景下问题不大,但放在低功耗BLE设备上就很吃亏,因为CPU必须一直醒着转圈,功耗压不下去。
BLE应用本质上是事件驱动的。设备大部分时间在等待,等待手机连接、等待传感器触发、等待某个定时器到期。用事件驱动模型来写,主流程可以随时休息,只有事件来的时候才醒来处理,处理完继续睡。
smartBASIC把这种模型直接做进了语言层面。它提供了一组on_开头的事件处理器,你只需要写好“事件来了之后干什么”,剩下的调度、睡眠、唤醒都由运行时帮你搞定。
举个例子:
on_ble_connected() // 手机连上了 printf("connected\n") end on_ble_disconnected() // 手机断开了 printf("disconnected\n") end这跟你平时写中断服务程序的思路很像,区别在于smartBASIC已经把这些事件底层的东西包装好了,不用你去翻协议栈的回调注册机制。
我推荐你写smartBASIC程序的时候,先把整个应用的事件列表列出来:哪些是GPIO触发的,哪些是BLE事件,哪些是定时器事件。列出之后你会发现,代码结构基本就出来了,剩下的事情就是在每个事件处理函数里填动作。
2.2 GATT是BLE世界里的“文件系统”
聊BLE应用,绕不开GATT。很多接触过BLE但没深入的人,对GATT的理解停留在“一种数据格式”上,其实用一个文件系统的类比会更直观:
- Service(服务)相当于文件夹,是某一类功能的集合;
- Characteristic(特征值)相当于文件夹里的文件,具体数据就存在这里;
- 每个Service和Characteristic都有一个UUID,相当于文件和文件夹的路径标识;
- 属性(Properties)相当于文件的访问权限,是只读、可写,还是支持通知。
smartBASIC里注册一个GATT服务,核心APi实际上就三步:先注册一个服务端实例,然后往里加Service和Characteristic,再给Characteristic设置属性。代码逻辑大概是这样的:
dim hServer hServer = ble_registergattserver() dim hBattService, hBattChar hBattService = ble_addattr(hServer, 0x180F) // Battery Service hBattChar = ble_addattr(hServer, 0x2A19) // Battery Level ble_addattrflag(hServer, hBattChar, 0x10) // 允许Notify0x180F和0x2A19都是Bluetooth SIG定义的16位标准UUID,一个代表电池服务,一个代表电量值。这样定义好之后,手机端就能通过标准BLE API读到这个服务。
在做这个部分时我踩过一个坑:只加了Characteristic,但没设置Notify属性,结果手机端一直收不到数据推送。后来查了官方文档才发现,Characteristic的属性标志决定了这个数据是只能被手机主动读,还是模块可以主动通知,两个能力是分开控制的。
2.3 smartBASIC的语法和常用API
smartBASIC的语法基础是BASIC系,跟Visual Basic有点像,但也有嵌入式语言特有的简洁。变量用dim声明,逻辑块用if/endif、for/next、while/wend,函数用function...end,事件处理器用on_.../end。
我刚接触的时候最不适应的点在于:smartBASIC不区分大小写,而且变量类型没那么严格。写惯了C语言的人可能觉得这有点随意,但反过来看,这恰恰降低了上手难度——你不必纠结int、uint8_t、size_t这些类型,声明一个变量就能存数值、字符串,用到哪算哪。
常用API我按功能分了一下类:
| 功能分类 | 典型API | 说明 |
|---|---|---|
| GPIO | gpiocfg / gpioset / gpioget | 配置方向、上下拉,读写引脚 |
| UART | uartopen / uartxwrite / uartread | 串口收发 |
| ADC | adcread | 读取模拟电压 |
| PWM | pwmopen / pwmwrite | 驱动蜂鸣器、LED调光 |
| BLE广播 | ble_setadvertisementdata / ble_startadvertising | 设置广播数据并启动广播 |
| BLE GATT | ble_registergattserver / ble_addattr / ble_senddata | 注册服务、添加属性、发送通知 |
| 定时器 | timer_start / on_timer | 周期执行任务 |
| 系统 | printf / sleep / reset | 打印日志、延时、复位 |
需要提醒一下,不同模块、不同固件版本的smartBASIC API名称可能有细微差别,实际开发时要以官方提供的smartBASIC Reference文档为准。我这里列出的API从BL600时代到BL654基本是兼容的,大方向不会有问题。
3. 实操:在BL654模块上跑通第一个smartBASIC工程
3.1 硬件准备:BL654模块、DVK-BL654开发板、串口
BL654是一颗基于Nordic nRF52840 SoC的BLE模块,支持BLE 5.0,自带天线或外接天线版本都有,算力在BLE模块里属于比较强的。它的smartBASIC运行时直接跑在nRF52840上,所以你能用到的外设资源也很可观:多个GPIO、UART、SPI、I2C、ADC、PWM,甚至还有USB外设接口。
我推荐有条件的直接上一块DVK-BL654开发板,不要一开始就自己画模块底板。开发板上面集成了USB转串口、几个按键、LED、温湿度传感器、光敏传感器,新手可以少踩很多硬件坑。我当时自己做底板时,就因为天线区域铺铜和匹配电路处理不当,导致信号差了一大截,后来换成官方开发板才定位到问题出在底板上。
开发板上电之后的接线很简单:USB线连接开发板和电脑,电脑上会出现一个虚拟串口。注意安装好串口驱动,BL654开发板用的是FTDI芯片,一般会自动识别,如果识别不到去官网拉一下驱动就行。
3.2 搭工具链:uSDK + UwTerminalX
smartBASIC开发离不开两样官方工具:uSDK和UwTerminalX。
- uSDK是软件包,里面包含了smartBASIC编译器、文档、示例代码、固件镜像。
- UwTerminalX是一个跨平台的串口终端工具,作用是连接模块、发送命令、上传脚本,调试时可以直接把UART日志打印到终端里。
我一般的工作流是:先用UwTerminalX连接模块,确认串口能通信,然后用uSDK里的编译器把smartBASIC源码编译成模块可执行的格式,最后在UwTerminalX里操作上传命令。
下载uSDK之后,建议先看一下核心文档,比如smartBASIC Reference Guide和BL654 User Guide。Laird的文档风格偏工程化,目录很清晰,遇到API不确定时的第一反应应该是查文档而不是搜论坛。
打开UwTerminalX后,选择正确的COM口和波特率。这里有个经验:BL654默认的串口波特率不一定是常见的115200,我的这块板子默认配对波特率就是115200,但有些固件版本是9600,连接不上时先试试几个常用波特率。连接成功后,在终端里输入AT指令,能看到模块返回OK,说明链路正常。
3.3 写一个按键控制LED的应用:从代码到跑通
我先从一个最简单的应用开始:一个按键控制一个LED翻转,同时把按键状态更新到广播数据里,让手机App能扫描到状态变化。这个小例程麻雀虽小,五脏俱全,涵盖了GPIO、事件处理、BLE广播三块核心内容。
看一下smartBASIC源码:
// smartBASIC demo: button toggles LED and updates adv data dim nLed, nBtn nLed = 1 nBtn = 2 // configure pins gpiocfg(nLed, 1, 0) // LED as output, no pull gpiocfg(nBtn, 0, 1) // button as input, pull-up // start advertising with a custom flag dim nAdvFlag nAdvFlag = 0 ble_setadvertisementdata(0, nAdvFlag) ble_startadvertising(0) // 0 = advertise indefinitely // event: button falling edge on_gpio(nBtn, 1) if nAdvFlag == 0 then nAdvFlag = 1 gpioset(nLed, 1) else nAdvFlag = 0 gpioset(nLed, 0) endif ble_setadvertisementdata(0, nAdvFlag) ble_startadvertising(0) end逐段解释一下:
nLed和nBtn是两个变量,用来存引脚编号。gpiocfg是引脚配置函数,三个参数分别是引脚号、方向、上拉模式。nLed作为输出,所以方向填1;nBtn作为输入,因为按钮一端接地,需要内部上拉,所以第三个参数填1。这里有个很容易弄反的点:很多人看到“上拉”就以为按键按下时读到高电平,实际上按钮接GND时,内部上拉让引脚默认高电平,按下瞬间被拉低,所以事件触发条件要选下降沿。on_gpio(nBtn, 1)里的1就是下降沿触发。
广播数据的设置使用ble_setadvertisementdata,第一个参数是广播句柄,单广播时通常填0,第二个参数是要放进广播包的数据。这里用nAdvFlag当状态位,方便手机端扫描解析。
编译时,在uSDK目录里找到编译器命令行工具,执行类似这样的操作:
sbc -o demo.bcm demo.swd其中demo.swd是smartBASIC源文件,demo.bcm是编译产物。编译过程都很顺利,如果打出error,大概率是API名称拼写或者参数个数不对,对照参考文档改了就好。
上传脚本到模块时,需要先让模块进入DFU模式。不同板子操作方法略有不同,我的DVK-BL654上是用跳线帽把DFU引脚拉高,然后按一下复位键,再插入USB,UwTerminalX里就能进入烧录模式。之后在UwTerminalX里选择“Send File”,把demo.bcm发进去,等进度条走完,复位模块,应用就跑起来了。
跑通那一刻还是有点小兴奋的:按下按钮,LED翻转,手机端用nRF Connect扫描能看到广播数据里的状态位同步变化,一个没有外部MCU的BLE节点就这样完成了。
3.4 进阶玩法:把传感器数据通过BLE通知发出去
按键控制LED只是热身,实际产品里更常见的需求是定时把传感器数据通过BLE通知发送给手机。比如一个电池供电的温度传感器,每隔500毫秒读一次ADC,把电压值换算成温度,然后通过GATT的Notify属性推给手机。
这个逻辑用smartBASIC写,核心其实就是一个定时器加一个通知函数:
dim hServer, hTempChar hServer = ble_registergattserver() ble_addattr(hServer, 0x1809) // Health Thermometer Service hTempChar = ble_addattr(hServer, 0x2A1C) // Temperature Measurement ble_addattrflag(hServer, hTempChar, 0x10) // Notify property on_timer(500) // every 500ms dim nRaw, nTemp nRaw = adcread(0) // read ADC channel 0 nTemp = nRaw * 100 / 4096 // simple conversion ble_senddata(hServer, hTempChar, nTemp) end timer_start(500)这里有一个值得注意的点:定时器一旦启动就会周期性触发,如果不及时睡眠,功耗会上去。如果是电池供电设备,建议把周期拉长,或者在数据没变化时跳过发送。另外,手机端必须订阅了该Characteristic的Notification,模块才能把数据推送过去,否则ble_senddata不会产生实际效果。我一开始没在手机端点击订阅按钮,结果模块这边数据一直在发,手机端却一点反应都没有,排查了半天才发现是订阅问题。
4. 常见问题与排查实录:脚本、事件、低功耗
4.1 串口连不上、脚本上传失败
这是新手遇到最多的一个问题。我在调试BL654时就遇到过串口识别不到、上传脚本一直卡住的情况,整理下来主要原因有三种:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 串口列表里找不到COM口 | 驱动没装好 | 重新安装FTDI/CP210x驱动,检查USB线是否数据线 |
| 能打开串口但无响应 | 波特率不对 | 依次尝试9600、115200、57600 |
| 脚本上传到一半失败 | 模块没正确进入DFU模式 | 检查DFU引脚设置,确认上传前模块处于Bootloader状态 |
再提醒一个细节:USB线一定要用能传输数据的线。现在很多Type-C线只能充电,插上去电脑完全没有反应,我因为这个浪费了半小时,后来换了一根好的USB线立刻就好了。
4.2 事件不触发、数据发不出去
代码逻辑看着没问题,但模块就是没反应,这类问题通常是以下几个原因:
- GPIOCFG方向配置反了。输入引脚配成了输出,事件永远不会触发。
- 上下拉设置不对。按钮接法跟pin配置不一致,导致电平状态完全反了。
- 广播数据超长。BLE广播包单包最多31字节,塞了太多数据会被截断或直接报错。
- GATT属性没开Notify。加了服务但没加属性标志,手机收不到通知。
- MTU太小。有些数据量较大的通知,一次可能发不完,需要协商MTU或者分帧发送。
我的排查习惯是先在事件处理器里加printf,把执行路径打印出来。比如事件到底有没有触发、参数是什么,一打印就知道,比自己瞎猜快得多。等到逻辑确认没问题,再把调试打印删掉。
4.3 低功耗和量产踩坑记录
smartBASIC的一大卖点是低功耗,但低功耗不是写几行sleep代码就能实现的,实测下来有几个经验:
- 尽量不要用轮询写业务逻辑。while循环读传感器虽然代码简单,但CPU一直在跑,功耗立刻上去。改成定时器事件或者GPIO中断唤醒,状态,不处理业务时让模块睡觉,功耗可以差出几个数量级。
- GPIO不要悬空。悬空的引脚会有漏电流,而且可能随机跳变产生误触发,白白唤醒模块。没有使用的引脚也要配置成固定电平或上拉。
- 调试时串口会显著增加功耗。printf输出会唤醒底层外设,测功耗时要把所有调试打印关掉。我一开始一直开着UART打印看温度曲线,结果测出来电流总有几毫安,以为代码有问题,关了打印瞬间降到微安级别。
- 量产烧录时,不要一台一台用UwTerminalX手动传。到小批量阶段建议准备一个批量烧录夹具,或者直接通过模块的SWD接口烧录,比串口方式稳定且快很多。手动串口烧录偶尔会遇到超时、丢包的问题,批量生产时完全不可接受。
5. 写在最后:一些实际体会
用smartBASIC做项目,是一种和传统MCU开发不太一样的体验。它不会给你C语言那种对底层完全掌控的感觉,但反过来,BLE协议栈那一大堆复杂细节也不由你操心,在中小规模的产品里,这种“够用就行、快速交付”的思路非常舒服。我个人的体会是,它特别适合低功耗传感器节点、便携设备、医疗健康类小批量产品这类场景,团队里如果没有人对BLE协议栈特别熟,smartBASIC能帮你绕开很多深坑。
如果你做的产品需要对数据吞吐量要求很高、或者要跑复杂的加密算法和UI逻辑,那smartBASIC目前未必是最佳选择,外挂MCU的多核方案会更合适。可如果你只需要一个稳定省电的BLE数据节点,它绝对值得认真评估。
最后分享一个我自己常用的扩展思路:smartBASIC模块不一定只是“模块”,它可以当作一个独立的低功耗协处理器来用。主系统有一颗应用处理器,smartBASIC模块负责跟手机通信和传感器采集,两边通过UART或者GPIO交换数据。这样主系统依然是自己熟悉的环境,BLE部分则交给smartBASIC处理,整个架构清晰又稳定。这个用法我在好几个项目里都验证过,效果一直很稳。