2024年寒假练 - 使用ATSAMD21实现基于串口的OTA升级
该项目使用了SAMD21G17D 微控制器,实现了基于串口的OTA升级的设计,它的主要功能为:固件启动引导及固件更新。
标签
嵌入式系统
串口
SAMD21
KCP
yekai
更新2024-03-28
160

平台介绍

本次使用的开发板为基于微芯SAMD21G17D的核心板和扩展板。微芯SAMD21G17D是一颗Cortex-M0+内核的处理器,可运行在48MHz的频率上,同时包含16KB的SRAM和128KB的Flash。扩展板包含了一系列的数字和模拟外设,便于开发和使用。

任务需求

本次我选择的任务是基于uart的OTA升级功能。

在此任务中,我通过OLED屏幕显示内容,触摸按键读取用户操作,通过核心板自带的调试器的串口与电脑上位机进行通信。具体实现了以下内容

  • OLED显示
  • 触摸按键读取
  • 串口交互
  • 固件完整性校验及拷贝启动

设计思路

固件主要分为三段,Bootloader、App_Slot1和App_Slot2,使用FAL进行Flash分区的管理。在Bootloader中判断App_Slot1中是否存在固件,并在不存在固件或是通过触摸按键停留在Bootloader等待上位机升级。同时在App内也能通过触摸按键重启进入Bootloader进行升级。

单片机和上位机交互主要使用了KCP进行交互,上位机将固件分片后推入KCP队列,KCP处理后通过串口打包下发,下位机串口解包后通过KCP解析后将固件写入App_Slot2。下发完后下发固件的CRC32进行校验,如无问题就拷贝App_Slot2到App_Slot1,并启动。

具体实现

BL向APP跳转及BL与APP交互

对于ARM处理器的跳转,我们可以根据其固件的结构通过固件开头的部分确定APP的栈顶和起始地址。

根据LD文件我们可以确定向量表置于Flash顶部

    .text : {
      . = ALIGN(4);
      _text_vectortable_start = .;
      KEEP(*(.vector_table))
      _text_vectortable_end = .;
      *(.text .text.*)
      *(.rodata .rodata*)

      KEEP (*(.init))
      KEEP (*(.fini))
  } > FLASH

观察向量表我们可以发现固件开头即为APP的栈顶和起始地址。

__attribute__ ((section(".vector_table"), used))
const H3DeviceVectors exception_table = {
       /* Configure Initial Stack Pointer, using linker-generated symbols */
      .pvStack = &_stack_end,

      .pfnReset_Handler              = Reset_Handler,
      .pfnNonMaskableInt_Handler     = NonMaskableInt_Handler,
... ...
      .pfnTCC3_Handler               = TCC3_Handler,
};

通过读取这些内容即可跳转

    const struct fal_partition *slot_1 = fal_partition_find("app_slot1");
   const struct fal_partition *slot_2 = fal_partition_find("app_slot2");
   if (info.ota_state == OTA_NONE) {
       fal_partition_read(slot_1, 0, (uint8_t *) &app_stack_top, 4);
       fal_partition_read(slot_1, 4, (uint8_t *) &app_entry, 4);
       if ((app_stack_top < 0x20000000) || (app_stack_top > 0x20000000 + 0x4000)) {
           elog_w(TAG, "app_stack_top invalid : 0x%08x", app_stack_top);
           OLED_ShowNoFirmware();
           Delay_ms(2000);
           goto _wait_ota;
      }
       if ((app_entry < slot_1->offset) || (app_entry > slot_1->offset + slot_1->len)) {
           elog_w(TAG, "app_entry invalid : 0x%08x", app_entry);
           OLED_ShowNoFirmware();
           Delay_ms(2000);
           goto _wait_ota;
      }
       // wait 5 s
       for (int i = 5; i > 0; i--) {
           OLED_ShowStartInS(i);
           auto tick = Tick_Get();
           while (tick + 1000 > Tick_Get()) {
               touch_process();
               if (key_cache != !!(get_sensor_state(0) & KEY_TOUCHED_MASK)) {
                   key_cache = !key_cache;
                   if (key_cache) {
                       //Touch detect
                       elog_i(TAG, "detect");
                       goto _wait_ota;
                  } else {
                       //Touch No detect
                       elog_i(TAG, "not detect");
                  }
              }
          }
      }
       // entry app
       elog_i(TAG, "entry app");
       elog_i(TAG, "app offset: 0x%08x", slot_1->offset);
       elog_i(TAG, "app stack top: 0x%08x", app_stack_top);
       elog_i(TAG, "app entry: 0x%08x", app_entry);
       // 记得关外部中断外设,否则会进HardFault
       EIC_InterruptDisable(EIC_PIN_11);
       EIC_REGS->EIC_CONFIG[0] = 0;
       EIC_REGS->EIC_CONFIG[1] = 0;
       EIC_REGS->EIC_CTRL |= EIC_CTRL_SWRST_Msk;
       while ((EIC_REGS->EIC_STATUS & EIC_STATUS_SYNCBUSY_Msk) == EIC_STATUS_SYNCBUSY_Msk) {}
       NVIC_DisableIRQ(SysTick_IRQn);
       NVIC_DisableIRQ(RTC_IRQn);
       NVIC_DisableIRQ(EIC_IRQn);
       NVIC_DisableIRQ(DMAC_IRQn);
       NVIC_DisableIRQ(SERCOM1_IRQn);
       __disable_irq();
       __set_MSP(app_stack_top);
       SCB->VTOR = ((uint32_t) slot_1->offset & SCB_VTOR_TBLOFF_Msk);
      ((void (*)(void)) (app_entry))();
  } else {
       elog_i(TAG, "ota state: %d", info.ota_state);
       info.ota_state = OTA_NONE;
  }

关于BL与APP的交互,我在内存中独立了一块区域以避免被初始化,

MEMORY
{
FLASH (rx) : ORIGIN = 64 * 1024 , LENGTH = 32* 1024
RAM (rwx) : ORIGIN = 0x20000000 , LENGTH = 0x4000 - 32
NOINIT (rw) : ORIGIN = 0x20000000 + 0x4000 - 32 , LENGTH = 32
}
SECTIONS
{
  .noinit : {
      . = ALIGN(4);
      _noinit_start = .;
      *(.noinit .noinit.*)
      . = ALIGN(4);
      _noinit_end = .;
  } > NOINIT
}

观察启动文件可以看到拷贝了data段清零了bss段,对于独立在后的noinit段不会修改,故可以在不断电情况下保存一些数据

    // Copy global variables from flash to ram
   uint32_t count = (&_data_end - &_data_start) * 4;
   __builtin_memcpy(&_data_start, &_data_flash, count);

   // Clear the bss segment
   __builtin_memset(&_bss_start, 0, (&_bss_end - &_bss_start) * 4);
typedef enum {
   OTA_NONE = 0,
   OTA_REQUEST,
   OTA_PROCESSING,
   OTA_DONE
} ota_state_t;

typedef struct {
   uint32_t reset_magic;
   ota_state_t ota_state;
} info_t;

__attribute__((section(".noinit"))) info_t info;

void Info_Init(void) {
   if (info.reset_magic == 0xdeadbeef) {
       return;
  }
   elog_i(TAG, "first boot, info init");
   info.reset_magic = 0xdeadbeef;
   info.ota_state = OTA_NONE;
}

然后就能从APP内重启到Bootloader进行升级

    // APP主循环
   while (1) {
       elog_i(TAG, "HelloWorld");
       gpio_out_toggle(led);
       auto tick = Tick_Get();
       while (tick + 1000 > Tick_Get()) {
           touch_process();
           if (key_cache != !!(get_sensor_state(0) & KEY_TOUCHED_MASK)) {
               key_cache = !key_cache;
               if (key_cache) {
                   //Touch detect
                   elog_i(TAG, "detect");
                   info.ota_state = OTA_REQUEST;
                   NVIC_SystemReset();
              } else {
                   //Touch No detect
                   elog_i(TAG, "not detect");
              }
          }
      }
  }
// Bootloader 即可获取到 OTA_REQUEST

内存管理

由于KCP需要使用动态内存,同时由于内存较小,静态分配可能导致可用内存较少,使用动态内存会更方便开发。我使用了tinyalloc管理内存。

在LD中确定好heap的区域,一般为bss尾端到stack前端,并用heap_startheap_end标记

    _Min_Stack_Size = 0x1000;
  .stack (NOLOAD) :
  {
      PROVIDE(end = .);
      PROVIDE(heap_start = .);
      . = . + _Min_Stack_Size;
  } > RAM

  _stack_end = 0x20000000 + 0x4000 - 32;
  _stack_start = _stack_end - _Min_Stack_Size ;
  heap_end = _stack_start;

之后将这段内存丢给tinyalloc处理

    extern uint8_t heap_start[];
   extern uint8_t heap_end[];
   ta_init(heap_start, heap_end, 256, 16, 4);
   elog_i(TAG, "heap_start: %p, heap_end: %p", heap_start, heap_end);

然后就能愉快的使用ta_alloc等api了

分区管理

我使用FAL进行分区管理具体分区如下

/* partition table */
// magicword               分区名           Flash 设备名 偏移地址   大小
#define FAL_PART_TABLE {           \
   {FAL_PART_MAGIC_WORD,   "bl",           "onchip",         0, 64*1024, 0}, \
   {FAL_PART_MAGIC_WORD,   "app_slot1",   "onchip",   64*1024, 32*1024, 0},     \
   {FAL_PART_MAGIC_WORD,   "app_slot2",   "onchip",   96*1024, 32*1024, 0},     \
   }
#endif /* FAL_PART_HAS_TABLE_CFG */

同时也需要调整BL与APP的LD文件中的地址

/* Bootloader */
MEMORY {
FLASH (rx) : ORIGIN = 0x0 , LENGTH = 0x10000
}
/* App */
MEMORY {
FLASH (rx) : ORIGIN = 64 * 1024 , LENGTH = 32* 1024
}

对片内Flash的适配如下

#include <fal.h>
#include <string.h>
#include "nvmctrl/plib_nvmctrl.h"

static int init(void) {
   /* do nothing now */
   return 0;
}

static int read(long offset, uint8_t *buf, size_t size) {
   memcpy(buf, (uint8_t *) offset, size);
   return size;
}

static int write(long offset, const uint8_t *buf, size_t size) {
   NVMCTRL_REGS->NVMCTRL_CTRLB &= ~NVMCTRL_CTRLB_MANW_Msk;
   for (int i = 0; i < size; i += 4) {
       *((uint32_t *) (i + offset)) = *((uint32_t *) (buf + i));
       while (NVMCTRL_IsBusy()) {
           __NOP();
      }
  }
   return size;
}

static int erase(long offset, size_t size) {
   for (int page = offset; page < offset + size; page += 0x100) {
       NVMCTRL_REGS->NVMCTRL_ADDR = page >> 1;

       NVMCTRL_REGS->NVMCTRL_CTRLA = (uint16_t)
              (NVMCTRL_CTRLA_CMD_ER_Val |
                NVMCTRL_CTRLA_CMDEX_KEY);
       while (NVMCTRL_IsBusy()) {
           __NOP();
      }
  }
   return size;
}

const struct fal_flash_dev onchip_flash = {
      .name       = "onchip",
      .addr       = 0x00000000,
      .len        = 128 * 1024,
      .blk_size   = 256,
      .ops        = {init, read, write, erase},
      .write_gran = 32
};

串口及前后台处理

我这边简单的使用接收中断和阻塞发送来处理串口收发,使用CherryRB进行缓冲

    const uint16_t mempool_size = 512;
   recv_mempool = ta_alloc(mempool_size);
   trans_mempool = ta_alloc(mempool_size);
   if (0 != chry_ringbuffer_init(&recv_rb, recv_mempool, mempool_size)) {
       elog_w(TAG, "chry_ringbuffer_init failed");
  }
   if (0 != chry_ringbuffer_init(&trans_rb, trans_mempool, mempool_size)) {
       elog_w(TAG, "chry_ringbuffer_init failed");
  }

串口接收中断后将收到的数据放入rb

void UART5_RecvCallBack(uint8_t byte) {
   chry_ringbuffer_write_byte(&recv_rb, byte);
}

在主循环中处理

    while (1) {
       while (!chry_ringbuffer_check_empty(&recv_rb)) {
           auto len = chry_ringbuffer_get_used(&recv_rb);
           auto *buf = (uint8_t *) ta_alloc(len);
           chry_ringbuffer_read(&recv_rb, buf, len);
           UartAnalyse(buf, len);
           ta_free(buf);
      }
       while (!chry_ringbuffer_check_empty(&trans_rb)) {
           auto len = chry_ringbuffer_get_used(&trans_rb);
           auto *buf = (uint8_t *) ta_alloc(len);
           chry_ringbuffer_read(&trans_rb, buf, len);
           elog_i(TAG, "send: %d bytes", len);
           elog_hexdump(TAG, 16, buf, len);
           SERCOM5_USART_Write(buf, len);
           ta_free(buf);
      }
       ikcp_update(kcp, Tick_Get());
       auto len = ikcp_recv(kcp, (char *) kcp_buf, 256);
       if (len > 0) {
           elog_i(TAG, "kcp recv: %d bytes", len);
           elog_hexdump(TAG, 16, kcp_buf, len);
           elog_i(TAG, "write %p", slot_2->offset + addr);
           fal_partition_write(slot_2, addr, kcp_buf, len);
           addr += len;
           crc32 = CRC_CalcArray_Software(kcp_buf, len, crc32);
      }
  }

串口协议及KCP

底层的串口协议简单的包裹了数据,并使用状态机进行解析。使用长度的最高位代表是KCP包还是CRC包

// 打包
int PackAndSend(const char *buf, int len, ikcpcb *kcp, void *user) {
   uint16_t new_len = 0;
   auto *p = ReplaceCRLF((const uint8_t *) buf, (uint16_t) len, &new_len);
   chry_ringbuffer_write_byte(&trans_rb, 0x5A);
   chry_ringbuffer_write_byte(&trans_rb, 0xA5);
   chry_ringbuffer_write_byte(&trans_rb, (uint8_t) new_len);
   chry_ringbuffer_write(&trans_rb, p, new_len);
   uint8_t sum = 0;
   for (uint8_t i = 0; i < new_len; i++) {
       sum += p[i];
  }
   chry_ringbuffer_write_byte(&trans_rb, sum);
   chry_ringbuffer_write_byte(&trans_rb, 0x0D);
   chry_ringbuffer_write_byte(&trans_rb, 0x0A);
   ta_free(p);
}
// 解包
void UartAnalyse(uint8_t *recv_buf, uint32_t len) {
   static uint8_t state = 0;
   static uint8_t frame_len = 0;
   for (uint32_t i = 0; i < len; i++) {
       uint8_t byte = recv_buf[i];
       switch (state) {
           case 0: {
               if (byte == 0x5A) {
                   state = 1;
              }
               break;
          }
           case 1: {
               if (byte == 0xA5) {
                   state = 2;
              } else {
                   state = 0;
              }
               break;
          }
           case 2: {
               if (byte & 0x80) {
                   elog_i(TAG, "cmd pack");
                   frame.len = byte & 0x7F;
                   frame.isCmd = true;
              } else {
                   frame.len = byte;
                   frame.isCmd = false;
              }
               frame_len = 0;
               state = 3;
               break;
          }
           case 3: {
               frame.data[frame_len] = byte;
               frame_len++;
               if (frame_len == frame.len) {
                   state = 4;
              }
               break;
          }
           case 4: {
               uint8_t sum_cal = 0;
               for (uint8_t j = 0; j < frame.len; j++) {
                   sum_cal += frame.data[j];
              }
               if (sum_cal != byte) {
                   elog_w(TAG, "sum error");
                   state = 0;
                   break;
              }
               state = 5;
               break;
          }
           case 5: {
               if (byte == 0x0D) {
                   state = 6;
              } else {
                   state = 0;
              }
               break;
          }
           case 6: {
               if (byte == 0x0A) {
                   elog_i(TAG, "recv: %d bytes", frame.len);
                   elog_hexdump(TAG, 16, frame.data, frame.len);
                   if (frame.isCmd) {
                       auto cmd = frame.data[0];
                       switch (cmd) {
                           case 0x01: {    // done, check crc32 and reset
                               uint32_t crc32_recv = frame.data[1] | (frame.data[2] << 8) |
                                                    (frame.data[3] << 16) | (frame.data[4] << 24);
                               if (crc32 == crc32_recv) {
                                   elog_i(TAG, "crc32 check pass");
                                   info.ota_state = OTA_NONE;
                                   CopyFirmware();
                                   NVIC_SystemReset();
                              } else {
                                   elog_w(TAG, "crc32 check fail, %08x != %08x", crc32, crc32_recv);
                                   info.ota_state = OTA_NONE;
                                   NVIC_SystemReset();
                              }
                               break;
                          }
                           default:
                               elog_w(TAG, "unknown cmd: %02x", cmd);
                               break;
                      }
                  } else {
                       auto new_len = (uint16_t) 0;
                       auto p = ReverseCRLF(frame.data, frame.len, &new_len);
                       memcpy(frame.data, p, new_len);
                       ta_free(p);
                       ikcp_input(kcp, (char *) frame.data, new_len);
                  }
              }
               state = 0;
               break;
          }
      }
  }
}

同样上位机里也有一份类似的代码

// 打包
function PackAndSend(port, data, len, cmd = false) {
   data = ReplaceCRLF(data, len)
   len = data.length
   let send_buf = new Uint8Array(len + 6);
   send_buf[0] = 0x5A;
   send_buf[1] = 0xA5;
   send_buf[2] = len & 0x7F;
   if (cmd) send_buf[2] |= 0x80;
   for (let i = 0; i < len; i++) {
       send_buf[i + 3] = data[i];
  }
   let sum = 0
   for (let i = 0; i < len; i++) {
       sum += data[i]
  }
   send_buf[len + 3] = sum & 0xFF;
   send_buf[len + 4] = 0x0D;
   send_buf[len + 5] = 0x0A;
   port.write(send_buf);
   console.log("send ", len, "bytes")
   console.log("send:", Uint8Array2Hex(send_buf));
}
// 解包
ota_port.on("data", (data) => {
   console.log(data)
   for (let i = 0; i < data.length; i++) {
       let value = data[i]
       switch (state) {
           case 0: {
               if (value === 0x5A) {
                   state = 1
              } else {
                   state = 0
              }
               break
          }
           case 1: {
               if (value === 0xA5) {
                   state = 2
              } else {
                   state = 0
              }
               break
          }
           case 2: {
               state = 3
               frame.len = value
               frame_len = 0
               frame.data = new Uint8Array(frame.len)
               break
          }
           case 3: {
               frame.data[frame_len] = value
               frame_len++
               if (frame_len === frame.len) {
                   state = 4
              }
               break
          }
           case 4: {
               let sum_cal = 0
               for (let i = 0; i < frame.len; i++) {
                   sum_cal += frame.data[i]
              }
               sum_cal &= 0xFF
               if (sum_cal !== value) {
                   console.log("sum error")
                   state = 0
                   break
              }
               state = 5
               break
          }
           case 5: {
               if (value === 0x0D) {
                   state = 6
              } else {
                   state = 0
              }
               break
          }
           case 6: {
               if (value === 0x0A) {
                   state = 0
                   console.log("recv ", frame.len, "bytes")
                   console.log("recv:", Uint8Array2Hex(frame.data))
                   let data_ = ReverseCRLF(frame.data, frame.len)
                   kcpobj.input(data_)
              }
               break
          }
      }
  }
})

解包完成后将数据丢入KCP处理。

对于KCP,基本使用了默认的配置,在下位机替换了内存分配函数

    const auto internal = 20;
   ikcp_allocator(ta_alloc, ta_free);
   kcp = ikcp_create(123, (void *) 0);
   kcp->output = PackAndSend;
   ikcp_nodelay(kcp, 0, internal, 0, 0);
   ikcp_setmtu(kcp, 60);
   ikcp_wndsize(kcp, 128, 128);
var kcpobj = kcp.KCP(123, ota_port)
const interval = 20;
kcpobj.nodelay(0, interval, 0, 0)
kcpobj.setmtu(60)
kcpobj.wndsize(128, 128)

在KCP发完后发送CRC32的值进行比对(这个node-kcp库使用node-addon直接给源代码包了一层,并没有使用Promise进行异步处理,故在此下发实现略显不优雅)

let i = 0
let crc32 = 0xffffffff
while (i < file_size) {
   let send = 32
   if (i + send > file_size) {
       send = file_size - i
  }
   let read_buffer = new Uint8Array(send)
   await file_handle.read(read_buffer, 0, send, i);
   kcpobj.send(read_buffer)
   i += send
   crc32 = CRC_CalcArray_Software(read_buffer, send, crc32)
   console.log("kcp send ", i, "bytes")
}
console.log("crc32:", crc32.toString(16).toUpperCase())
const handle = setInterval(() => {
   if (kcpobj.waitsnd() === 0) {
       setTimeout(() => {
           clearInterval(kcp_handle)
           let buf = new Uint8Array(
              [0x01, crc32 & 0xFF, (crc32 >> 8) & 0xFF, (crc32 >> 16) & 0xFF, (crc32 >> 24) & 0xFF]
          )
           PackAndSend(ota_port, buf, buf.length, true)
           console.log("send crc32:", Uint8Array2Hex(buf))
      }, 2000)
       clearInterval(handle)
  }
}, 50)

同时下位机也进行CRC32计算比对,并在一致后拷贝固件

uint32_t CRC_CalcArray_Software(uint8_t *data, size_t len, uint32_t crc_value = 0xffffffff) {
   const uint32_t st_const_value = 0x04c11db7;
   auto data_32 = reinterpret_cast<uint32_t *>(data);

   for (uint32_t i = 0; i < len / 4; i++) {
       uint32_t xbit = 0x80000000;
       for (uint32_t bits = 0; bits < 32; bits++) {
           if (crc_value & 0x80000000) {
               crc_value <<= 1;
               crc_value ^= st_const_value;
          } else {
               crc_value <<= 1;
          }
           if (data_32[i] & xbit) {
               crc_value ^= st_const_value;
          }
           xbit >>= 1;
      }
  }
   return crc_value;
}        
   // 主循环中
   ikcp_update(kcp, Tick_Get());
   auto len = ikcp_recv(kcp, (char *) kcp_buf, 256);
   if (len > 0) {
       elog_i(TAG, "kcp recv: %d bytes", len);
       elog_hexdump(TAG, 16, kcp_buf, len);
       elog_i(TAG, "write %p", slot_2->offset + addr);
       fal_partition_write(slot_2, addr, kcp_buf, len);
       addr += len;
       crc32 = CRC_CalcArray_Software(kcp_buf, len, crc32);
  }
// 解析状态机中
   case 0x01: {    // done, check crc32 and reset
       uint32_t crc32_recv = frame.data[1] | (frame.data[2] << 8) |
          (frame.data[3] << 16) | (frame.data[4] << 24);
       if (crc32 == crc32_recv) {
           elog_i(TAG, "crc32 check pass");
           info.ota_state = OTA_NONE;
           CopyFirmware();
           NVIC_SystemReset();
      } else {
           elog_w(TAG, "crc32 check fail, %08x != %08x", crc32, crc32_recv);
           info.ota_state = OTA_NONE;
           NVIC_SystemReset();
      }
       break;
  }
uint32_t CopyFirmware() {
   const struct fal_partition *slot_1 = fal_partition_find("app_slot1");
   const struct fal_partition *slot_2 = fal_partition_find("app_slot2");
   elog_i(TAG, "Start to copy firmware");
   elog_i(TAG, "erasing slot 1");
   fal_partition_erase(slot_1, 0, slot_1->len);
   const auto len = slot_2->len;
   const auto cache_size = 256;
   auto *cache = (uint8_t *) ta_alloc(cache_size);
   uint32_t addr = 0;
   while (addr < len) {
       uint16_t read_len = len - addr > cache_size ? cache_size : len - addr;
       elog_i(TAG,"copying from %p to %p, %d bytes", addr + slot_2->offset, addr + slot_1->offset, read_len);
       fal_partition_read(slot_2, addr, cache, read_len);
       fal_partition_write(slot_1, addr, cache, read_len);
       addr += read_len;
  }
   ta_free(cache);
}

OLED

我这边简单的适配了下u8g2,通过软件模拟i2c

#include <stdio.h>
#include "u8g2_port.h"
#include "user_gpio.h"
#include "u8g2.h"
#include "cmsis_compiler.h"
#include "Tick.h"
#include "elog.h"
#include "tinyalloc.h"

static const char OLED_I2C_ADDR = 0x78;

u8g2_t u8g2;

struct gpio_out *i2c_sda;
struct gpio_out *i2c_scl;

static const char *TAG = "u8g2_port";

uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
   switch (msg) {
       case U8X8_MSG_DELAY_100NANO: // delay arg_int * 100 nano seconds
           for (int i = 0; i < arg_int; i++) {
               __NOP();
          }
           break;
       case U8X8_MSG_DELAY_10MICRO: // delay arg_int * 10 micro seconds
           for (int i = 0; i < 100 * arg_int; i++) {
               __NOP();
          }
           break;
       case U8X8_MSG_DELAY_MILLI:   // delay arg_int * 1 milli second
           Delay_ms(arg_int);
           break;
       case U8X8_MSG_DELAY_I2C:     // arg_int is the I2C speed in 100KHz, e.g. 4 = 400 KHz
           for (int i = 0; i < 10; i++) {
               __NOP();
          }
           break;                    // arg_int=1: delay by 5us, arg_int = 4: delay by 1.25us
       case U8X8_MSG_GPIO_I2C_CLOCK: // arg_int=0: Output low at I2C clock pin
           gpio_out_write(*i2c_scl, arg_int);
           break;                    // arg_int=1: Input dir with pullup high for I2C clock pin
       case U8X8_MSG_GPIO_I2C_DATA:  // arg_int=0: Output low at I2C data pin
           gpio_out_write(*i2c_sda, arg_int);
           break;                    // arg_int=1: Input dir with pullup high for I2C data pin
       case U8X8_MSG_GPIO_MENU_SELECT:
           u8x8_SetGPIOResult(u8x8, /* get menu select pin state */ 0);
           break;
       case U8X8_MSG_GPIO_MENU_NEXT:
           u8x8_SetGPIOResult(u8x8, /* get menu next pin state */ 0);
           break;
       case U8X8_MSG_GPIO_MENU_PREV:
           u8x8_SetGPIOResult(u8x8, /* get menu prev pin state */ 0);
           break;
       case U8X8_MSG_GPIO_MENU_HOME:
           u8x8_SetGPIOResult(u8x8, /* get menu home pin state */ 0);
           break;
       default:
           u8x8_SetGPIOResult(u8x8, 1); // default return value
           break;
  }
   return 1;
}

void OLED_Init() {
   i2c_sda = ta_alloc(sizeof(struct gpio_out));
   i2c_scl = ta_alloc(sizeof(struct gpio_out));
   if ((i2c_scl == NULL) || (i2c_sda == NULL)) {
       elog_e(TAG, "malloc failed");
       return;
  }
   *i2c_sda = gpio_out_setup(GPIO('A', 12), 0);   // PA12 as I2C SDA
   *i2c_scl = gpio_out_setup(GPIO('A', 13), 0);  // PA13 as I2C SCL

   u8g2_Setup_ssd1306_i2c_128x64_noname_f(
           &u8g2,
           U8G2_R0,
           u8x8_byte_sw_i2c,
           u8x8_gpio_and_delay
  );

   u8g2_SetI2CAddress(&u8g2, OLED_I2C_ADDR);

   u8g2_InitDisplay(&u8g2);

   u8g2_SetPowerSave(&u8g2, 0);

   u8g2_ClearBuffer(&u8g2);
   u8g2_SendBuffer(&u8g2);
}

void OLED_ShowStartInS(uint32_t s) {
   u8g2_ClearBuffer(&u8g2);
   u8g2_SetFontDirection(&u8g2, 0);
   u8g2_SetFont(&u8g2, u8g2_font_crox2c_mr);   // width 9
   char *str = "BootLoader";
   uint8_t len = u8g2_GetStrWidth(&u8g2, str);
   u8g2_DrawStr(&u8g2, (128 - len) / 2, 16, str);
   u8g2_SetFont(&u8g2, u8g2_font_courB08_tf);   // width 9
   char time[32];
   snprintf(time, 32, "Will start in %ld s", s);
   len = u8g2_GetStrWidth(&u8g2, time);
   u8g2_DrawStr(&u8g2, (128 - len) / 2, 40, time);
   char *ota_str = "Press Touch to ota";
   len = u8g2_GetStrWidth(&u8g2, ota_str);
   u8g2_DrawStr(&u8g2, (128 - len) / 2, 60, ota_str);
   u8g2_SendBuffer(&u8g2);
}

void OLED_ShowNoFirmware() {
   u8g2_ClearBuffer(&u8g2);
   u8g2_SetFontDirection(&u8g2, 0);
   u8g2_SetFont(&u8g2, u8g2_font_crox2c_mr);   // width 9
   char *str = "No Firmware";
   uint8_t len = u8g2_GetStrWidth(&u8g2, str);
   u8g2_DrawStr(&u8g2, (128 - len) / 2, 28, str);
   u8g2_SendBuffer(&u8g2);
}

void OLED_ShowWaitOTA() {
   u8g2_ClearBuffer(&u8g2);
   u8g2_SetFontDirection(&u8g2, 0);
   u8g2_SetFont(&u8g2, u8g2_font_crox2c_mr);   // width 9
   char *str = "Wait OTA";
   uint8_t len = u8g2_GetStrWidth(&u8g2, str);
   u8g2_DrawStr(&u8g2, (128 - len) / 2, 28, str);
   u8g2_SendBuffer(&u8g2);
}

触摸

简单的使用微芯提供的生成器配置了一下,并循环调用即可

    // 等待按下触摸按键来阻止启动时
   for (int i = 5; i > 0; i--) {
       OLED_ShowStartInS(i);
       auto tick = Tick_Get();
       while (tick + 1000 > Tick_Get()) {
           touch_process();
           if (key_cache != !!(get_sensor_state(0) & KEY_TOUCHED_MASK)) {
               key_cache = !key_cache;
               if (key_cache) {
                   //Touch detect
                   elog_i(TAG, "detect");
                   goto _wait_ota;
              } else {
                   //Touch No detect
                   elog_i(TAG, "not detect");
              }
          }
      }
  }

实现的效果

显示

传输

占用

遇到的问题及解决方案

  • 跳转神奇的卡死进HardFault,原来是跳之前EIC的配置要清空一下
  • newlib自带的内存分配器似乎有些问题,容易分配出NULL来,使用了tinyalloc进行替换。
  • 通过串口阻塞打log可能会显著的影响实时性,尤其是当前代码存在很多hexdump处,可用使用DMA进行处理,我这边简单的拉高波特率到6000000同时使用CH347接收。

优化的方向

  • 串口波特率较高时(实测250000及以上)可能会导致数据的堆积造成一些问题,可能需要使用DMA来解决,或使用主频更高的处理器。同时调试器也仅支持较低的波特率(500000及以下),稍显遗憾。
  • 当前Flash占用较高,同时也没有加入固件签名、安全启动等功能,还是比较遗憾。

总结和致谢

在这个项目中我也算是跑通了Bootloader固件更新的流程,坑点蛮多,但也还是顺利。靠开源库的组合拳还是快速的开发出了项目的整体样子,但最终还是免不了使用微芯的触摸库。项目中许多东西还是对linkerscript和startup的熟悉程度要求较高,蛮锻炼人。

感谢klipper项目、Arduino项目的部分源码,让我能较为顺利的在官方支持不佳时搭建基于cmake+gcc的项目。


附件下载
eetree_samd21_app.7z
app源码
eetree_samd21_bl.7z
bootloader源码
EETREE_OTA_Serial.7z
上位机
firmware.7z
固件
团队介绍
一个人的团队
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号