Nordic 遇到的问题

在项目中使用的52840搭载的FreeRTOS操作系统,使用的是SES开发环境而非Keil(因为Nordic买了SES的版权,使用SES不会有版权问题)

NRF52840使用记录

在项目中使用的52840搭载的FreeRTOS操作系统,使用的是SES开发环境而非Keil(因为Nordic买了SES的版权,使用SES不会有版权问题)

52840 相关

1、P0.9和P0.10

由于52840支持NFC功能,因此该两个引脚默认为NFC引脚,在项目中如果用作普通的GPIO,则需要进行设置。

需要在system_nrf52.c中加宏定义

#define CONFIG_NFCT_PINS_AS_GPIOS

2、SPI问题

在nrf52840做主机时,需要使用DMA进行发送,但是由于SPI的easyDMA(不只是SPI)的长度限制为不超过256字节,因此,在我的项目中,由于使用的是每包512字节的机制,所以需要进行分包发送。还出现过uninit时,while循环导致看门狗重启,由从机导致。需要注意此点。

3、I2C问题

目前有很多单片机中的I2C变成了TWI。TWI全兼容I2C,但并不局限于I2C,双线发送均可使用TWI进行操作。

TWI在操作上较I2C复杂,需要自己写一个I2C的接口,使用TWI进行封装

4、FDS问题

  1. 需要对数据长度有较好管控,否则一旦读取的长度比实际长度长时,可能会导致HardFault,原因是fds不对地址溢出进行校验,因此长度过长,超出FLASH的映射空间,就会有HardFault产生。
  2. 读FLASH数据时,需要使用memcpy将数据读出,否则可能会导致HardFault,原因是fds返回的地址并非是内存地址(0x2000000起始),而是flash地址(0x0起始),在执行一些操作时,由于地址不是内存地址而产生
  3. fds不对数据进行加密,当使用flash读取工具时,(jFlash,nrf—connect)则会读出存储的设备信息,可能有很关键的信息,这个是需要考虑的点,当然也可以在产品出厂时设置为flash不可读取,但是也会有一些不方便之处
  4. 我实测一次gc的时间大概是800ms左右
  5. 推荐进行封装成write和read两个函数,否则操作很麻烦

5、堆栈问题

nrf52840的例程中,使用的heap较小,当开辟较大数据时,容易出现开辟失败的情况,如果需要的话,需要对heap进行扩容。也需要保证malloc和free的匹配性,当然也可以使用官方的内存管理机制,为原子操作,更放心,但是需要对其较为了解。

6、GPIO中断问题

GPIO本来是只支持8个引脚的中断,但是使用PORT,还有sdk_config.h中的宏,来进行规避此问题,例如我的项目中就高达10个中断。

使用该方式时,有可能需要将结构体元素中的hi_accuracy进行赋值为1,否则可能会有检测不到中断的可能性!!!但会增加部分功耗(10uA)

7、低功耗问题

  1. 起始芯片自身的功耗是非常少的,只有2~3uA,因此即使是一个小电容也可能供电一段时间,这也是一个调低功耗的坑,断电时间过短,由于电容供电,导致芯片还在工作一点时间,需要把握好这个时间节点,也可以使用一些外部的短路等快速放电
  2. 蓝牙的功耗,蓝牙的功耗是随着广播时间的间隔或者打开关闭来改变的,但是广播也和设备连接有一定的关系,广播越久,功耗越低,设备连接越慢,需要进行取舍。我的项目中,测试大概是1s广播间隔,连接时间最长可能达到十多秒,平均功耗在20uA左右,可以尝试了解快慢广播的概念,然后进行配置,在两者之间做出平衡
  3. 蓝牙的引脚,蓝牙的引脚默认都是disconnect的状态,因此,不配置就是最低的功耗。低功耗应用场景,一般都有对于外围芯片电源的控制,因此即开即关也是一个功耗处理点,对于不需要关闭电源的设备,一般来说会有加速度传感器,加速度传感器一般的选型是LiS2DH,该芯片的内部有一个上拉电阻,功耗可达百微安级别,需要设置成不可使用上下拉,另外一个采样频率也是一个功耗点,我的项目中为25Hz,浮点型计算也是一个功耗点
  4. MCU的外设也是需要即开即关,但是有可能会init和uninit不同步导致的断言问题,因此需要控制好
  5. 蓝牙引脚设置为默认状态时,外部设备的连接为浮空状态,可能为高电平,该状态可能与外部IC冲突,导致的功耗问题,我的项目中需要和另一个wifi芯片进行通讯,wifi芯片的唤醒中断引脚,因为这个默认状态而导致的休眠失败是一个功耗点
  6. 如果使用浮点型计算,千万要记得关闭FPU,该寄存器会有大概6mA左右电流

8、复位问题

nrf芯片可以记录复位原因,包括复位引脚,上电,看门狗,也可以用户在需要重启的时候写入寄存器,自定义原因,在上电时,就可以进行分别处理,我的项目中,是做了非用户操作的重启,则用户不感知重启的功能。重启不清零数据的内存地址放到了 attribute((section(“.non_init”)));如果有bootloader,需要同步设置

9、重启定位问题

  1. APP_ERROR_CHECK,在app_error.c文件夹中,可以打印出具体的文件名字和行号、以及错误代码,较为简单。
  2. HardFault,在hardfault_implementation.c文件中,已经详细对其做出了打印,则根据PC指针,LR寄存器等,来推断导致hardfault的位置。LR寄存器一般存放产生hardfault的函数,pc指针定位到了具体的代码地址
  3. 看门狗复位,看门狗是为了防止程序异常死机,但是正常情况下也是不允许该情况的,可以在看门狗中断函数中,读出传参sp[6]的值,该值就是PC指针,然后记录下来,放到重启不清零的RAM地址(attribute((section(“.non_init”)))),即可进行打印地址,定位问题。
  4. 用户复位就不需要分析了吧

10、PC指针

这个可以通过map文件搜索,和debug模式下搜索,可以找到具体的代码,进行定位问题。

或者使用objdump对elf进行反汇编。需要安装

arm-none-eabi 工具。如下命令:

arm-none-eabi-objdump -D “T7400_1.0.0.4.elf” > T7400_1.0.0.4.S

在生成汇编文件后,即可进行查看汇编代码查看具体问题

11、fstorage问题

默认队列为4,当队列用完时,会返回错误码,此处导致的问题需要注意;

nrf_fstorage_write flash等函数,操作数据源地址(*p_src)也需要四字节对齐,可使用 attribute((aligned(4)))进行对齐。并且flash写入是异步操作,所以数据源地址(*p_src)要注意不要使用局部变量;

12、日志问题

由于我的项目中,软件测试是用串口接收打印,而不是使用Jlink,因此需要将数据通过串口输出,但是当使用串口进行输出时,出现的异常重启则不可以进行打印,导致分析出现问题。因此需要将APP_ERROR_CHECK和HardFault的数据也通过串口输出出来。

下面为我的调试代码:用于重大错误即将重启之前的调用

nrf_drv_uart_t m_uart = NRF_DRV_UART_INSTANCE(UART_LOG_INSTANCE);
static volatile bool m_xfer_done;
//发送完成中断函数
static void uart_evt_handler(nrf_drv_uart_event_t *p_event, void *p_context)
{
    m_xfer_done = true;
}

//重大错误调用函数,在HardFault中也可调用
void _log_error(char *fmt, uint16_t len)
{
    //反初始化串口,这个地方需要注意,因为不知道有没有进行开始串口,内部可通过标志位或者事件进行对应处理
    uart_driver_deinit();
    
    //初始化串口
    nrf_drv_uart_config_t config = NRF_DRV_UART_DEFAULT_CONFIG;
    config.pseltxd = UART_LOG_TX_PIN;   //只需要用到发送串口
    config.pselrxd = NRF_UART_PSEL_DISCONNECTED;
    config.pselcts = NRF_UART_PSEL_DISCONNECTED;
    config.pselrts = NRF_UART_PSEL_DISCONNECTED;
    config.baudrate = (nrf_uart_baudrate_t)NRF_LOG_BACKEND_UART_BAUDRATE;
    nrf_drv_uart_init(&m_uart, &config, uart_evt_handler);

    //日志进行格式化,带颜色较为清晰
    uint8_t err_buf[256];
    snprintf(err_buf, sizeof(err_buf), "\r\n\033[31m<<<<<reboot>>>>>:\033[0m\033[33m %s\033[0m", fmt);

    m_xfer_done = false;

    //进行数据输出
    nrf_drv_uart_tx(&m_uart, (uint8_t *)err_buf, strlen(err_buf));

    //tick用来确保中断被关闭时,也能正常退出
    uint32_t tick = 1000000;
    while (m_xfer_done == false && tick-- != 0)
    {
    }
    nrf_drv_uart_uninit(&m_uart);

    //调试使用,连上jlink时,也可进行jlink输出
    NRF_LOG_FLUSH();
}

13、调试相关

使用jlink调试,可以查看所有内存信息和堆栈调用。参考博客 https://github.com/meishaoming/blog/issues/53

14、各组件打开功耗

外设 运行功耗
CPU 3~6mA
Radio 4~16mA
SAADC 1.24mA
PWM 0.2mA
WDT 0.003mA
GPIOTE 0.017mA
DMA 2mA
FPU 6mA
I2C 0.1mA
SPI 0.1mA
adv 0dBm 1s广播 0.022mA

操作系统相关

1、内存问题

在所有使用freertos的工程中,一般默认推荐的都是使用的heap_4这个内存管理机制。

该内存管理机制是用FreeRTOS进行统一管理,FreeRTOS使用的所有内存都从一个数组中进行开辟。而Nordic官方使用的heap_1作为例程。当进行任务开辟时,非常容易由于总共的堆栈空间分配不足,导致失败。因此我后来也是选用了heap_4这个内存管理机制

但是当任务堆栈不足时,也会导致任务开辟失败。可通过修改configTOTAL_HEAP_SIZE的值进行处理。默认为4k,几个任务就不够了。

需要注意的问题是,你任务中使用的所有变量,局部变量、函数调用占用的空间等,都是占用的任务的堆栈空间,malloc函数占用的是程序的堆空间。因此要保证开辟任务时,保证开辟的任务堆栈空间不足,否则就会导致重启,可通过configCHECK_FOR_STACK_OVERFLOW为1,在钩子内进行打印堆栈溢出的任务名字,进行确认。还可以通过使用INCLUDE_uxTaskGetStackHighWaterMark的方式来查看任务的最深调用。当接近0时,需要增加任务堆栈,当一直远远大于0时,可考虑进行减少任务堆栈。

需要注意可能并不是所有任务都初始化时就开始,有些任务可能开辟,不久就释放,所以测试时,需要进行全部任务开启测试,保证运行中开辟任务时,出现内存不够的问题。

2、中断问题

由于FreeRTOS的机制问题,需要区分中断的快速操作处理还是非中断模式下的可延时等待处理,因此给了两个函数,中断中需要调用带FromISR的函数,因此在写代码时需要非常注意。但是可以通过一个函数来进行判断0 == __get_CONTROL()

if (0 == __get_CONTROL()) // in isr handler
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(_QueueHandle, &info, &xHigherPriorityTaskWoken);
}
else
{
    xQueueSend(_QueueHandle, &info, wait_time);
}

3、打印与功耗问题

由于nordic移植的函数 configUSE_IDLE_HOOK一般为0,不会有功耗问题,但是打印不全,因为没有位置调用NRF_LOG_FLUSH函数,进行日志输出。但是当该值设置为1时,打印完全,但是芯片却没有休眠(8mA)线程未休眠都是8mA左右,一直在执行该函数。

知道该原因也就好解决了,我的采用方式是,定义自己的日志输出函数,通过串口输出,当自己的日志进行输出时,也直接调用NRF_LOG_FLUSH函数。

4、优先级问题

nordic的任务优先级configMAX_PRIORITIES中最大为3 ,可能不满足任务要求,推荐经典值16 ,与uCOSII不同,该值越大,优先级越高

当高优先级的任务产生时,会直接打断正在运行的低优先级任务,因此很多函数可能需要做好重入处理。比如外设的init和uninit函数。否则由于该问题导致的重启就很难找到。

5、定时器问题

nordic的定时器任务深度默认只有80,当项目中增加定时器,需要注意此处导致的重启configTIMER_TASK_STACK_DEPTH,我的项目中应用的为1k

6、任务名字问题

nordic的默认名字configMAX_TASK_NAME_LEN长度为4 ,推荐根据具体项目具体更改。

7、看门狗问题

建议看门狗单独做一个任务,且任务优先级为1,不能和其他任务同时使用,否则则可能出现任务中的其他功能阻塞掉看门狗任务,导致看门狗重启。

8、系统时钟问题

nordic默认 configTICK_RATE_HZ 宏为1024,但是如果使用该值,所有延时时间都会提前结束,一秒钟提前几个毫秒,累加提前,十分钟大概12~13s。需要将该宏修改为1000,或者延时,乘上节拍系数 。该值为portTICK_PERIOD_MS 。但是如果任务调度过多,该方式可能会导致延时稍晚结束。但比1024准确很多,如需必须很精准定时,需要使用rtc时间

9、调试相关

可以在hard fault函数里打印当前任务名字,当前堆栈值,可通过堆栈值进行判断。堆栈深度和堆栈初始值,可以判断是否堆栈溢出,部分堆栈溢出不会调用钩子,直接被hard fault了。

在task.c里添加如下函数即可。

void xTaskStackShow(TCB_t *pxTCB)
    {
        volatile unsigned long *stack;
        unsigned long addr1;
        unsigned long addr2;
        unsigned long addr3;
        unsigned long addr4;
        if (pxTCB == NULL)
            return;

        stack = (volatile unsigned long *)pxTCB->pxTopOfStack;

//打印当前任务所有堆栈信息
        _log_error("dump task:[%s] pxStack %x,depth %x", pxTCB->pcTaskName, pxTCB->pxStack, pxTCB->uxStackDepth);
        while (stack < ((unsigned long *)(pxTCB->pxStack) + (UBaseType_t)(pxTCB->uxStackDepth)))
        {
            addr1 = *stack++;
            addr2 = *stack++;
            addr3 = *stack++;
            addr4 = *stack++;
            anker_log_error("[<%08lx>] [<%08lx>] [<%08lx>] [<%08lx>] ", addr1, addr2, addr3, addr4);
        }
//打印当前任务名字
    char * xTaskGetCurrentTaskName(void)
    {
        TCB_t *task = xTaskGetCurrentTaskHandle();
        return task->pcTaskName;
    }

//打印当前任务信息
    void xTaskStackShowCurrentTask(void)
    {
        xTaskStackShow(xTaskGetCurrentTaskHandle());
    }

uxStackDepth该成员变量官方库并未给出,需要自己在TCB_t中添加

注意添加位置和添加类型.。再创建任务时进行赋值


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!