平台介绍
本次使用的开发板为基于微芯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_start
与heap_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的项目。