一、 项目概述
本项目使用Funpack第四季第1期活动的板卡,Qorvo 的 DWM3001CDK,实现了任务一。
具体要求是:点灯!使用导线缠绕成NFC线圈连接在板上,通过刷卡控制板卡上的RGB LED,并实现切换至少4种不同灯效,(如色彩渐变,呼吸/流水效果等)。
二、 硬件介绍
开发板实物图
Qorvo 的 DWM3001CDK 是为基于 Qorvo DW3110 IC 的 DWM3001C 全集成 (UWB) 模块开发的设计套件。
开发板使用了DWM3001C模组,且板载 J-Link 调试器为 DWM3001C 提供 SWD 和 UART 接口,具有复位和用户定义按钮和LED。
模组支持NFC、BLE、UWB等无线功能。
DWM3001C模组内部框图如下图所示,
DWM3001C模组内部的处理器是Nodic公司的nRF52833,通过SPI口连接了型号为DW3110的UWB芯片。
本项目实现的NFC点灯功能,只需将NFC天线引出即可,NFC天线接口为板卡的J8口。控制灯效时,使用板载的D9-D12等4个LED。
三、 搜集资料的过程
搜集资料,主要通过以下途径。
硬禾学堂网站。下面网址提供了DWM3001C DK开发板的主要介绍、使用教程及常用链接。https://www.eetree.cn/platform/3617
Qorvo官网。官网https://www.qorvo.com/products/p/DWM3001CDK 提供了DWM3001CDK详细的介绍及开发包,开发包里包含了使用SEGGER Embedded Studio实现的例程,注意开发包需要使用企业邮箱下载。
Segger网站。https://www.segger.com/ 通过Segger网站可下载SEGGER Embedded Studio开发软件及JLink软件包。
Github网站。https://github.com/qqice/Work-on-DWM3001CDK 提供了Arduino环境的构建方法,可根据相关文档,实现Arduino对DWM3001C DK编程。
四、 软件开发
本项目在Windows环境下,使用SEGGER Embedded Studio进行开发。
4.1 实现过程介绍
第一步,熟悉开发板硬件。了解按钮和LED控制的接口函数。
先实现按钮控制LED效果。
获取按钮状态bButtonState=bsp_board_button_state_get(BSP_BOARD_BUTTON_0);
控制LED,
bsp_board_led_on(BSP_BOARD_LED_0);
bsp_board_led_off(BSP_BOARD_LED_1);
bsp_board_leds_on();
bsp_board_leds_off();
第二步,熟悉SDK_BSP里的NFC例程。
在SDK_BSP\Nordic\NORDIC_SDK_17_1_0\examples\nfc\record_text\pca10040目录下,有一个nfc刷卡的例程,手机靠近开发板NFC天线时,会有NFC弹窗。这个例程适用的是nRF52832的开发板,稍加改动后,即可适用于nRF52833,同时也适用于DWM3001C模组及其开发板。
具体改动为,
在项目option菜单下,修改Target Device类型,由nRF52832改成nRF52833。
编译时,如果报错。将工程里的SEGGER_RTT_Syscalls_SES.c文件删除。
同时,需要修改flash_placement.xml文件,将“”里面的0x4删除。
然后,下载测试NFC,手机靠近开发板NFC天线时,会有弹窗显示。
第三步,测试PWM功能。
SDK_BSP\Nordic\NORDIC_SDK_17_1_0\examples\peripheral\pwm_driver下,提供了一个PWM控制LED的例程,同样,对程序按照上一步的过程,使其适配nRF52833。
为了匹配开发板的引脚及端口,还需要修改板卡类型,由PCA10040板卡改成CUSTOM定制板卡。这一步,板卡类型设置,无法在文件中修改,在option菜单下也无法修改。
经过查找资料,可以在pwm_driver_pca10040.emProject工程文件上,右键用记事本或VScode打开,直接将BOARD_PCA10040改成BOARD_CUSTOM即可。
然后,将custom_board.h文件,
放到SDK_BSP\Nordic\NORDIC_SDK_17_1_0\components\boards文件夹内。
这个例程里有5个demo,对于不同的pwm效果。
第四步,通过按钮,控制PWM功能。
将第一步和第三步的程序进行整合。
第五步,通过NFC,控制PWM功能。
将第二步和第三步的程序进行整合。
NFC的操作,和按钮操作是相似的,手机靠近和离开,可以看成是按钮的动作。
第六步,将上面的工程整合成综合实例。
整合后的实例,可以通过按钮或手机NFC贴卡的方式,控制PWM点灯效果。
4.2程序介绍
有两种方式,可以实现不同灯效的切换,分别是状态机方式和事件触发方式。
状态机和事件触发是两种常见的程序结构设计方法,它们在设计理念、适用场景和实现方式上都有显著的区别。
以下是它们的详细对比:
• 状态机:
核心思想:将程序的行为划分为多个状态,每个状态对应一种特定的行为模式。程序的行为变化通过状态之间的转换来实现。
设计重点:定义状态集合、状态之间的转换条件以及每个状态下的行为。
适用场景:适用于具有明确状态划分和状态转换逻辑的系统,例如协议解析、用户界面状态管理、有限状态自动机等。
• 事件触发:
核心思想:程序的行为由事件驱动,事件的产生和处理是程序运行的核心。事件可以是用户操作、系统消息、定时器触发等。
设计重点:定义事件类型、事件的产生机制以及事件的处理逻辑。
适用场景:适用于需要对多种事件进行响应的系统,例如图形用户界面(GUI)、实时系统、消息驱动的中间件等。
下面是主要代码介绍,重点介绍两种程序结构。
点灯子函数,主要来自官方SDK包,可查看代码后用AI分析,此处不再详细介绍。
4.2.1状态机
定义1个iLED_State表示当前LED状态。当按钮动作或手机NFC靠近时,iLED_State值自加一次,主程序根据iLED_State值,切换不同的子程序。
状态机变量
bool bButtonState; //按钮状态
bool bButtonLastState;
bool bNFC_WakeState;//NFC 状态 1-贴卡激活 0-卡拿开
bool bNFC_LastWakeState;
#define MAX_REC_COUNT 3
#define LED_STATE_NUM 6 /*LED状态数 */
bool bStateRunOnce[LED_STATE_NUM + 1] = {false}; // 子程序只执行一次标志位
int iLED_State; //状态机当前状态
NFC回调函数
当手机贴近开发板线圈或离开时,有状态标志变化
static void nfc_callback(void * p_context, nfc_t2t_event_t event, const uint8_t * p_data, size_t data_length)
{
(void)p_context;
switch (event)
{
case NFC_T2T_EVENT_FIELD_ON:
//bsp_board_led_on(BSP_BOARD_LED_0);//D9 绿灯
bNFC_WakeState=true;
break;
case NFC_T2T_EVENT_FIELD_OFF:
// bsp_board_led_off(BSP_BOARD_LED_0);
bNFC_WakeState=false;
break;
default:
break;
}
}
main函数
首先初始化板卡和NFC,然后在while循环里获取按钮或NFC动作,当检测到动作后,改变LED灯的状态变量iLED_State,根据iLED_State的状态,去执行不同的子程序。
需要注意的是,对子程序要进行区分,有的是只执行一次,有的是需要循环执行。
int main(void)
{
uint32_t len = sizeof(m_ndef_msg_buf);
uint32_t err_code;
log_init();
bsp_board_init(BSP_INIT_LEDS|BSP_INIT_BUTTONS);
APP_ERROR_CHECK(nrf_drv_clock_init());
nrf_drv_clock_lfclk_request(NULL);
APP_ERROR_CHECK(app_timer_init());
err_code = nfc_t2t_setup(nfc_callback, NULL);
APP_ERROR_CHECK(err_code);
err_code = welcome_msg_encode(m_ndef_msg_buf, &len);
APP_ERROR_CHECK(err_code);
err_code = nfc_t2t_payload_set(m_ndef_msg_buf, len);
APP_ERROR_CHECK(err_code);
err_code = nfc_t2t_emulation_start();
APP_ERROR_CHECK(err_code);
while (1)
{
bButtonState=bsp_board_button_state_get(BSP_BOARD_BUTTON_0);//Botton0对应开发板的SW2
if ((bButtonState)||((bNFC_WakeState==true) && (bNFC_LastWakeState==false)))
{
iLED_State=iLED_State+1;
nrf_delay_ms(300);
}
bNFC_LastWakeState=bNFC_WakeState;
if (iLED_State>LED_STATE_NUM)
{
iLED_State=0;
}
switch (iLED_State) {
case 0:
if (bStateRunOnce[0]==false)
{
pwm_uninit();
}
update_state_run_once(iLED_State);
bsp_board_leds_off();//全灭
break;
case 1:
if (bStateRunOnce[1]==false)
{
pwm_uninit();
demo1(); //轮流呼吸,只执行1次
}
update_state_run_once(iLED_State);
break;
case 2:
if (bStateRunOnce[2]==false)
{
pwm_uninit();
demo2(); //同时呼吸后闪烁,只执行1次
}
update_state_run_once(iLED_State);
break;
case 3:
if (bStateRunOnce[3]==false)
{
pwm_uninit();
demo3();//绿灯闪烁三次后,熄灭
}
update_state_run_once(iLED_State);
break;
case 4:
if (bStateRunOnce[4]==false)
{
pwm_uninit();
}
running_lights(200);//流水灯 快
update_state_run_once(iLED_State);
break;
case 5:
if (bStateRunOnce[5]==false)
{
pwm_uninit();
}
running_lights(500);//流水灯 慢
update_state_run_once(iLED_State);
break;
case 6:
if (bStateRunOnce[6]==false)
{
pwm_uninit();
}
bsp_board_leds_on();
update_state_run_once(iLED_State);
break;
default:
break;
}
}
}
4.2.2事件触发
程序结构:主程序里一直执行检测事件和处理事件两个子函数,根据事件的变化,去执行不同的子程序。
定义事件枚举变量
typedef enum {
EVENT_NONE = 0, // 无事件
EVENT_BUTTON_PRESS, // 按键按下
EVENT_NFC_WAKE, // NFC 激活
EVENT_MAX // 事件类型数量
} event_type_t;
事件入队
//事件队列 入队:将一个事件添加到事件队列中。如果队列已满,则不添加事件。
void event_enqueue(event_type_t event) {
if ((event_queue_head + 1) % EVENT_QUEUE_SIZE != event_queue_tail) { // 检查队列是否已满
event_queue[event_queue_head] = event;
event_queue_head = (event_queue_head + 1) % EVENT_QUEUE_SIZE;
}
}
事件出队
//出队:从事件队列中取出一个事件。如果队列为空,则返回 EVENT_NONE 。
event_type_t event_dequeue(void) {
if (event_queue_head != event_queue_tail) {
event_type_t event = event_queue[event_queue_tail];
event_queue_tail = (event_queue_tail + 1) % EVENT_QUEUE_SIZE;
return event;
}
return EVENT_NONE; // 如果队列为空,返回 EVENT_NONE
}
检测事件
void detect_events(void) {
bool button_state = bsp_board_button_state_get(BSP_BOARD_BUTTON_0);
if (button_state && !bButtonLastState) { // 检测按钮按下
nrf_delay_ms(200);
event_enqueue(EVENT_BUTTON_PRESS);//按钮按下
}
bButtonLastState = button_state; // 更新按钮状态
if (bNFC_WakeState && !bNFC_LastWakeState) {
nrf_delay_ms(200);
event_enqueue(EVENT_NFC_WAKE);//NFC贴过卡
}
bNFC_LastWakeState = bNFC_WakeState;
}
处理事件
//处理事件
void process_events(void) {
event_type_t event = event_dequeue(); // 获取第一个事件
void (* const demos[])(void) = {demo1, demo2, demo3, demo4, demo5,demo6};
uint8_t const demo_idx_max = (sizeof(demos) / sizeof(demos[0])) - 1;
static uint8_t demo_idx = 0;
while (event != EVENT_NONE) { // 如果事件不为空
switch (event) {
case EVENT_BUTTON_PRESS:
//按钮事件
if (demo_idx < demo_idx_max)
{
++demo_idx;
}
else
{
demo_idx = 0;
}
break;
case EVENT_NFC_WAKE:
// NFC 激活事件的处理逻辑
if (demo_idx < demo_idx_max)
{
++demo_idx;
}
else
{
demo_idx = 0;
}
break;
default:
break;
}
// 停止当前使用的 PWM 实例
if (m_used & USED_PWM(0)) {
nrf_drv_pwm_uninit(&m_pwm0);
}
if (m_used & USED_PWM(1)) {
nrf_drv_pwm_uninit(&m_pwm1);
}
if (m_used & USED_PWM(2)) {
nrf_drv_pwm_uninit(&m_pwm2);
}
m_used = 0;
// 启动新的演示
demos[demo_idx]();
// 获取下一个事件
event = event_dequeue();
}
}
主程序
主程序里只要循环检测和处理事件即可。
int main(void)
{
uint32_t len = sizeof(m_ndef_msg_buf);
uint32_t err_code;
log_init();
bsp_board_init(BSP_INIT_LEDS | BSP_INIT_BUTTONS);
APP_ERROR_CHECK(nrf_drv_clock_init());
nrf_drv_clock_lfclk_request(NULL);
APP_ERROR_CHECK(app_timer_init());
// NFC 初始化
err_code = nfc_t2t_setup(nfc_callback, NULL);
APP_ERROR_CHECK(err_code);
err_code = welcome_msg_encode(m_ndef_msg_buf, &len);
APP_ERROR_CHECK(err_code);
err_code = nfc_t2t_payload_set(m_ndef_msg_buf, len);
APP_ERROR_CHECK(err_code);
err_code = nfc_t2t_emulation_start();
APP_ERROR_CHECK(err_code);
demo1();
// 主循环
while (1) {
detect_events(); // 检测事件
process_events(); // 处理事件
}
}
4.2.3总结:
优缺点
• 状态机:
• 优点:
逻辑清晰:状态和状态转换逻辑明确,易于理解和维护。
可预测性高:状态转换是基于明确的条件,程序行为可预测。
适合复杂逻辑:对于具有复杂状态转换的系统,状态机能够很好地组织代码。
• 缺点:
状态爆炸:随着状态和转换条件的增加,状态机可能会变得复杂,难以管理。
灵活性差:状态机的逻辑相对固定,难以动态调整。
• 事件触发:
• 优点:
响应性强:能够快速响应各种事件,适合实时系统。
灵活性高:事件处理逻辑可以动态调整,适合动态变化的系统。
解耦性好:事件的产生和处理可以分离,便于模块化设计。
• 缺点:
逻辑分散:事件处理逻辑分散在多个处理函数中,难以集中管理。
复杂度高:对于复杂的事件依赖关系,代码可能会变得难以维护。
性能问题:事件队列和分发机制可能会引入额外的性能开销。
五、 效果演示
通过操作按钮或手机NFC贴近开发板线圈后离开,两种方式,实现了不同的点灯效果。可以实现依次呼吸灯、同时呼吸后闪烁、流水灯、跑马灯等多种效果。
具体演示效果无法通过图片描述,详见视频。
六、 心得体会
本次使用的硬禾学堂与得捷电子举办的Funpack第四季第1期的活动,收获很大。
本期的板卡Qorvo 的 DWM3001CDK开发板,具有比较丰富的无线功能,可以开发NFC、蓝牙、UWB等多种应用。
除了使用SEGGER Embedded Studio进行开发之外,也支持Nordic的NCS系统,以及Arduino等多种开发平台。
在开发过程中,也可以选择FreeRTOS、Zephyr等嵌入式操作系统或使用BSP进行裸机开发。
当然,开发过程中也遇到一些问题,
比如:安装jLink软件后,无法直接支持 DWM3001CDK开发板,需要手动更新驱动。
使用SEGGER Embedded Studio进行开发时,工程的文件目录不能太深,否则可能出现编译错误。
另外,对SEGGER Embedded Studio软件的使用还不太熟悉,没有经过系统的学习。
不过,随着AI助手的普及推广,很多问题都得到了快速的解决。
感谢组织者及技术交流群的小伙伴!