AT32开发踩坑记录

众所周知的原因,最近芯片有点涨价(st你又不是美国公司你涨啥 摔)

让我们强烈谴责js趁乱投机倒把的行为

于是就顺手尝试一下国产mcu咯

at32这玩意,小产品上用的挺多,看了下主流型号价格不贵内存flash给的挺舍得的,外设也不错

于是搞了个核心板来品一下他的库到底有多少屎…

芯片的型号是AT32F403ACGT7

这玩意好像就几块钱,M4内核,240M主频,1024K flash 200多k ram,挺厉害

image-20250506031323264

工作流

大致看了下,这玩意sdk支持Keil,IAR还有他们自己做的AT32 IDE(应该是类似Cube IDE的Eclipse魔改类似物)

从简单的角度就直接用keil了

看下官方提供的资源https://www.arterytek.com/cn/product/AT32F403A.jsp#Resource

image-20250506031757670

当然先下载个固件库,看看里面有啥例程,然后下载下keil5的支持包,安装一下

jlink识别包我这里没用到先不装

他竟然也有个类似于cube mx的图形化配置器,不错哦

直接找个例程看看

image-20250506032411062

工程都是现成的,很好

用keil打开尝试了一下,ok可以编译没问题,算是成功一半了(

在官网的技术支持里有很多ap note可以看,比如入门指南:

https://www.arterytek.com/download/APNOTE/AN0023_AT32F403A_407_Get_started_guide_V2.0.5_ZH.pdf

算是写的挺详细的了

其实似乎也没啥需要多说的

后面的使用基本上都是用他的配置器直接生成的工程,用起来和cube差不多吧,虽然简陋一点,姑且功能是ok的,所以这些例程基本上是参考代码作用了,也不需要手动按照这些例程去配置工程和系统初始化啥的,算是方便很多

基本上就和用Cube一样配好外设,时钟树,生成工程,写用户代码编译下载就行。

得说开发起来有配置器哪怕再简陋也方便太多了啊(主要是不用配该死的keil工程和include了。。。)

关于调试器

官方说是需要用他们的at link(其实看起来就是个改了个名字的dap link,在keil设置里就选dap link就可以了)

我这里实际试了下,用dap-link v2和stlink v2(淘宝最便宜的十块钱调试器)都是可以在keil正常识别和下载的,keil的调试也可以用

至于官方的下载工具,我没有去尝试,有兴趣的可以试一下

关于库

官方例程里有些调用就挺抽象的。。比如io的例程里

image-20250506033055298

不是,我说咱们IO操作库呢?就这么用寄存器怼啊?

但是实际上…官方是提供了库函数的

image-20250506033324889

固件库的文档里也有写明白

image-20250506033440390

image-20250506033431468

所以…what can i say呢?

关于串口printf

官方的ap note里有个串口printf的移植文档

但是!不要看那个

用不了,那文档2021年的,一直没更新,里面写的方法根本没法用(

what can i say

最后去例程里抄了一个,能用了

手动在main的user code里加下面这段就行

建议开micro lib(虽然他说可以不用开…但是实际上我试了下不开也不会省空间的…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/* support printf function, usemicrolib is unnecessary */
#if (__ARMCC_VERSION > 6000000)
__asm (".global __use_no_semihosting\n\t");
void _sys_exit(int x)
{
x = x;
}
/* __use_no_semihosting was requested, but _ttywrch was */
void _ttywrch(int ch)
{
ch = ch;
}
FILE __stdout;
#else
#ifdef __CC_ARM
#pragma import(__use_no_semihosting)
struct __FILE
{
int handle;
};
FILE __stdout;
void _sys_exit(int x)
{
x = x;
}
/* __use_no_semihosting was requested, but _ttywrch was */
void _ttywrch(int ch)
{
ch = ch;
}
#endif
#endif

#if defined (__GNUC__) && !defined (__clang__)
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif

/**
* @brief retargets the c library printf function to the usart.
* @param none
* @retval none
*/
PUTCHAR_PROTOTYPE
{
while(usart_flag_get(USART1, USART_TDBE_FLAG) == RESET);
usart_data_transmit(USART1, (uint16_t)ch);
while(usart_flag_get(USART1, USART_TDC_FLAG) == RESET);
return ch;
}

#if (defined (__GNUC__) && !defined (__clang__)) || (defined (__ICCARM__))
#if defined (__GNUC__) && !defined (__clang__)
int _write(int fd, char *pbuffer, int size)
#elif defined ( __ICCARM__ )
#pragma module_name = "?__write"
int __write(int fd, char *pbuffer, int size)
#endif
{
for(int i = 0; i < size; i ++)
{
while(usart_flag_get(USART1, USART_TDBE_FLAG) == RESET);
usart_data_transmit(USART1, (uint16_t)(*pbuffer++));
while(usart_flag_get(USART1, USART_TDC_FLAG) == RESET);
}

return size;
}
#endif

st7789驱动,spi和dma

必不可少的当然是点屏

我这里用一个烂大街的320*240 7789测试

7789库很多,我这里用的朋友移植的版本:https://github.com/Floyd-Fish/ST7789-STM32

首先说下这个spi

这玩意…官方例程里的写法是有问题的

他在每次spi发送之前判断txbuffer是否为空,如果为空就发送

如果单纯是发spi,这么写没问题

但是问题在于,tx buffer空 不代表spi传输完成 这时候可以写下一个buffer,但是实际上时序上,总线上数据还没有被发出(硬件phy有延时)

所以你写完spi之后拉cs pin是会直接挂死…(总线还没传输完就拉掉cs自然会挂掉…)

就这么个问题查了我一天,给我搞麻了,示波器波形看着都挺好的(毕竟没有仔细对比时序),最后还是靠逻辑分析仪出马才解决的

经过反复尝试后确定可以正确运行的方法是判断

while(spi_i2s_flag_get(SPI1, SPI_I2S_BF_FLAG) == SET);

也就是spi总线非忙的时候才可以进行下一步操作,这样可以保证spi传输完成,总线已经被释放

解决这个当然就已经可以点亮了,但是这样写spi很慢,还是得做dma

1
2
3
4
5
6
7
8
9
10
11
12
while(spi_i2s_flag_get(SPI1, SPI_I2S_BF_FLAG) == SET);//判忙

wk_dma_channel_config(DMA1_CHANNEL1,
(uint32_t)&SPI1->dt,
buff,
chunk_size);
dma_channel_enable(DMA1_CHANNEL1, TRUE);

while(dma_flag_get(DMA1_FDT1_FLAG) == RESET);
dma_flag_clear(DMA1_FDT1_FLAG);

dma_channel_enable(DMA1_CHANNEL1, FALSE);

这里首先还是得判忙

然后配置dma的源数据buffer和数据长度(dma初始化之前已经配置好了,直接用她的配置器生成)

最后启用dma开始传输

等待传输完成,记得这里要清掉完成flag 这tm竟然不会读的时候自动清除,如果不清下一次传输就不会等待会直接跳过,然后给关掉,就炸了…我真是服了

最后还得给dma关了(不然没法执行下一次重新配置传输buffer和开启传输)

记得给main里自动生成的配置和启动代码给关了(不然就在初始化的时候直接给开启传输了。。。)

image-20250506035306944

然后这一块玩意还不是用户代码,如果你在配置软件里改了重新生成,就会自动给覆盖回去又打开了…what can I say

lvgl移植

这东西吧 其实没啥太多要说的

不过在keil里移植的话,因为众所周知keil的工程文件管理特别逆天(不支持直接添加文件夹和子文件夹,库文件结构复杂的时候一个个加巨逆天),所以很折磨

这里有个简单点的办法(at32的文档写的,还挺好用)

image-20250506035845557

去下载好的lvgl里面找到这个pack,安装到keil里

然后工程里找这个

image-20250506035919924

勾上必要的组件(这里开了跑分和demo,如果只是使用只开essential就好了)

image-20250506035957244

直接ok就行了,会自动把lvgl必要的玩意都给你加到工程里

方便很多

image-20250506040031851

当然有个坏处就这些小钥匙都是只读的,实际上是直接引用的库路径,没法修改(但是实际上也不会没事去改这些源代码,只要改port代码就行了,所以不影响)

实际需要用到修改的就这五个文件(lv_conf_cmsis实际上就是lv_conf.h,内容是一样的)

image-20250506040150561

这里移植过程不多说了,所有的lvgl都是一样的,照着做就行了,没啥特别的

就是配置分辨率,配置buffer方式(单片机用模式1,开个10行buffer就行了),写flush刷屏函数,配触摸或者按键,systick里加服务函数这些东西 基本上是大同小异的

几个需要注意的点:

keil编译器要开c99

image-20250506040506168

用spi接口的情况要开字节序翻转,不然颜色会炸

image-20250506040540140

如果跑demo最少需要48k分配空间,跑自己的小ui可以开小

image-20250506040609981

systick的中断里面加上时基handler,这里要注意at32的配置器里systick那里要选中断模式,不然不会开systick中断,这里进不去

image-20250506040738203

刷新函数,很简单,供参考(write data里面是上文里的dma传输代码)

size *2是因为他传进来的像素区域是rgb565也就是int16的,按字节传输是两倍大小

image-20250506040855407

最后一点,如果你下载之后发现直接进hard fault挂死,那是因为爆栈了…

去配置器里给堆栈空间改大然后再跑就好了

跑个分(很简单,include benchmark的.h然后在初始化之后调用benchmark函数就行了)

img

关于gui制作,用按键做输入的移植,推荐下文章,写的很好(移植也可以参考他的系列,虽然是esp32的但是基本上一样)

https://blog.csdn.net/m0_55986987/article/details/133032918

https://blog.csdn.net/m0_55986987/article/details/133171860

移植FreeRTOS

其实我之前没用过rtos…

但是还是得学的吧

实际上移植意外的简单就是了,挺小的,比起lvgl那一大坨简单多了(

at32有这个文档,可以参考,里面还有rtos教程(乐

https://www.arterytek.com/download/APNOTE/AN0025_FreeRTOS_on_AT32_MCU_ZH_V2.0.0.pdf

虽然at32的官方固件库里提供了一个freertos版本,不过看了下那个玩意内核版本是10.4.3,我去官网下了个最新的内核版本是11.1.0,所以还是用自己下的吧

image-20250506161131522

我直接找了个gpio测试的工程去移植

理论上应该修改延时时钟源为除了systick的其他定时器,因为rtos会占用systick,不过我这里实测你不在配置器里开systick延时的中断模式的话,也能直接移植就是了,rtos能正常工作

但是大概是freertos的时钟源配置会覆盖原始的配置还是啥原因,启动了rtos调度器之后在里面用单片机库函数自带的delay是会直接飞掉(根本不延时,跑的飞快)

所以实际上如果需要用原来库函数的延时,就还是换个定时器吧。当然因为可以用vtaskdelay所以可能直接关了就好?

freertos官网下载最新版解压之后把这坨文件扔到工程目录里建个文件夹放进去就行

image-20250506161819672

在keil里加一下,文件不多不费劲

image-20250506161918119

其实就7个根目录下面的.c,还有protable下面的memmang里的heap_4,还有protable下面的rvds下面CM4F(我们单片机是CM4内核)里面port.c

就这9个文件

然后加include不提(include文件夹,cm4f文件夹,memmang里面没.h不用加)

重要!!

这里注意,如果你是keil5,AC6编译器,不能选RVDS目录下面的,要用GCC目录下面的CM4F文件夹里的文件

不然编译会报一万个错误…查了下说是ac5和ac6的兼容问题,ac6只支持gcc格式的一些语法,所以会报错

用gcc版本的文件是正常的

image-20250506162326654

记得选C99,freertos要求c99版本

最后加config.h,我直接从上面那个at32文档配套的例程里扣了一个,直接扔到工程的inc文件夹下面完事

image-20250506162521021

这里重要的主频和tick频率已经给配置好了(如果用的不一样,手动改一下)

堆栈大小优先级数量啥的,可以自己配,我先不动了

image-20250506163432792

编译一下,最后剩下的三个报错,就是中断handler重复定义,去工程生成的int.c里给报错的三个函数注释掉就行了(因为rtos会接管这些函数)

image-20250506162636167

注释掉应该就可以正常编译了,尝试写个简单的测试程序

需要包含一下rtos的库

1
2
#include "FreeRTOS.h"
#include "task.h"

创建线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void task1(void * para)
{
for(;;)
{
gpio_bits_toggle(GPIOA, GPIO_PINS_2);
printf("running in task 1...\n");
vTaskDelay(500);
}
}

void task2(void * para)
{
for(;;)
{
gpio_bits_toggle(GPIOA, GPIO_PINS_1);
printf("running in task 2...\n");
vTaskDelay(1000);
}
}

在外设初始化完成之后,启动线程和调度器

1
2
3
xTaskCreate(task1,"LED1",128,NULL,1,NULL);
xTaskCreate(task2,"LED2",128,NULL,1,NULL);
vTaskStartScheduler();

注意这里启动之后后面的while1就不会被执行到了

image-20250506162829390

下面的部分永远也不会被执行了,程序被调度器接管了

下载一下,两个灯都正常闪,移植ok

关于rtos的更多用法,还在学习中…

待续…