引言
电子森林(eetree.com)在2024年的“寒假一起练”的活动中,推出了很多开发平台供开发者学习和试用,其中,来自美国的半导体芯片原厂Microchip的SAMD21微控制器,因其集成了触摸感应模块(Peripheral Touch Controller),成功捕获了我的注意。我下定决心,借着这个宝贵的机会,利用2024年春节假期的时间(大约7天),试用一番SAMD21微控制器,希望了解Microchip微控制器的开发生态,学习Microchip微控制器的IP外设模块的设计和应用模型。在本次活动中,我将要通过完成一系列自行设计的任务,来学习这一款基于Arm核心的微控制器。
任务清单:
- 在Keil MDK环境下适配SAMD21的开发。
- 在
arm-mcu-sdk
项目中添加SAM21芯片及其开发板nano-samd21
,适配源代码工程框架。 - 创建(或移植)必要的简单驱动,包括时钟系统、引脚复用功能、GPIO、UART,并创建对应的样例工程。
- 创建(或移植)更多的外设驱动,包括通信类外设、定时器类外设、模拟类外设、以及USB和Touch外设的驱动和样例工程。
但实际上,由于时间有限,以及在开发过程中也遇到了一些情况,导致实际完成的工作量未达到预期,特别比较遗憾的是,暂未用起来USB和Touch等比较有特色的外设模块。然而,一些基本的开发工作已然完成,读者可以通过了解我的学习过程,非常简易地入门SAMD21微控制器,能够完成一些入门常用的小实验。在后续的学习过程中,如果时间允许,随着进一步阅读手册和搜集参考源码,还可以逐步丰富驱动程序和样例工程,从而实现更多有趣的功能。
本文记录了我在学习SAMD21的过程中阅读手册、适配开发环境,以及编写源代码调试各模块的全过程,以一个微控制器的资深玩家快速学习一款新芯片的视角,对一些要点进行了简要但清晰地梳理。
本文开发的软件包托管在在gitee.com的开源代码仓库中:git@gitee.com:suyong_yq/arm-mcu-sdk-release.git
搭建基于Keil和arm-mcu-sdk的开发环境
使用Keil开发SAMD21
基于Arm Cortex-M内核的微控制器进行软件开发,使用Keil的用户最多,群众基础好,各种深入浅出的技术资料众多,界面友好。
笔者在Keil(Arm)的官网上找到了Keil支持SAMD21的插件包。
- Keil官网上SAMD21设备支持包的主页:https://www.keil.arm.com/packs/samd21_dfp-microchip/devices/
- Keil的SAMD21设备支持包的直接下载地址:https://packs.download.microchip.com/Microchip.SAMD21_DFP.3.6.144.pack
下载下来之后发现这个设备支持包的体积比较庞大,里包含的设备型号也很多,本着精简使用的原则,我提取了其中必要的文件,包括描述芯片寄存器映射的SVD文件,以及下载算法文件,并精简了设备支持包的psdc资源描述文件,重新制作了一个专用于SAMD21G17D的设备支持包,体积缩减了将近200倍!如图x所示。
图x 原厂的设备支持包和定制的精简设备支持包
经验证,Keil可以正常识别。如图x所示。
图x 在Keil中使用精简的SAMD21设备支持包
跟这个pack支持包相关的开发要点有两个:
- 使用原厂pack包中的
ATSAMD21G17D.svd
文件,配合Keil自带的SVDConv
工具,使用fields=macro fields=enum
参数生成了CMSIS规范风格的芯片头文件samd21g17d.h
,这个文件将会在适配arm-mcu-sdk的过程中使用到。关于SVDConv
的详细用法可见Keil帮助中心的文档:https://www.keil.com/pack/doc/CMSIS/SVD/html/svd_SVDConv_pg.html - 精简pack文件的资源描述文件,删掉大量不相干的内容(对同系列其它芯片型号的描述)。精简之后的资源描述文件
Microchip.SAMD21G17D.pdsc
的内容如下:
<?xml version="1.0" encoding="utf-8"?>
<package schemaVersion="1.2" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xs:noNamespaceSchemaLocation="PACK.xsd">
<vendor>Microchip</vendor>
<name>ATSAMD21</name>
<url>http://www.microchip.net/microcontroller/pack/</url>
<description>Release version of Microchip SAMD21 Device Family Pack.</description>
<supportContact>support@microchip.com</supportContact>
<releases>
<release version="0.0.1">
Release version of Microchip SAMD21 Device Family Pack.
</release>
</releases>
<keywords>
<keyword>Microchip</keyword>
<keyword>SAMD21</keyword>
</keywords>
<devices>
<family Dfamily="ATSAMD21 family" Dvendor="Microchip:3">
<processor Dcore="Cortex-M0+" Dendian="Little-endian" Dmpu="NO_MPU" Dfpu="NO_FPU" />
<description>
Microchip SAMD21 family
</description>
<subFamily DsubFamily="ATSAMD21">
<description>
Microchip SAMD21 Series Device Support
</description>
<device Dname="ATSAMD21G17D">
<processor Dcore="Cortex-M0+" Dendian="Little-endian" Dmpu="NO_MPU" Dfpu="NO_FPU" />
<debug svd="SVD/ATSAMD21G17D.svd" />
<memory name="IROM1" start="0x00000000" size="0x00020000" startup="1" default="1"/>
<memory name="IROM2" start="0x00400000" size="0x1000"/>
<memory name="IRAM1" start="0x20000000" size="0x00004000" default="1"/>
<algorithm name="Flash/ATSAMD21_128.FLM" start="0x00000000" size="0x00020000" default="1"/>
<algorithm name="Flash/ATSAMD21_128_EEPROM.FLM" start="0x00400000" size="0x00001000" default="1"/>
</device>
</subFamily>
</family>
</devices>
</package>
在arm-mcu-sdk中适配SAMD21
为什么要适配arm-mcu-sdk,主要原因是还是从学习外设的角度出发,想深入了解硬件系统的工作机制。同时arm-mcu-sdk的驱动,相对于原本提供的样例工程,设计得比较简单,对底层软件的开发者更加友好。
arm-mcu-sdk中汇总了众多基于Arm内核的微控制器的外设驱动,以及典型的样例工程,SAMD21可以直接兼容已有的成熟案例,可以让开发者像使用其它流行的微控制器一样使用这款新加入的SAMD21微控制器。
准备必要的源码文件
在arm-mcu-sdk\devices
目录中创建SAMD21G17D
目录,在这个目录下将包含同SAMD21微控制器芯片相关的核心源码文件,其中大部分文件,可参照别的Arm核芯片,参考原厂设备支持包中的同名文件改写。包括:
samd21g17d.h
芯片头文件。使用SVDConv
工具从原厂设备支持包中的SVD文件转换得来。将被所有的SAMD21工程引用。system_samd21g17d.c
和system_samd21g17d.h
文件。这是CMSIS规范的启动代码中的C语言部分,主要提供SystemInit()
函数的实现。将被所有的SAMD21工程引用。mdk\startup_samd21g17d.s
文件。这是CMSIS规范的启动代码中的汇编语言部分,适配于各不同的编译器,本例中仅适配了Keil的AC6编译器,主要提供中断向量表的定义,以及复位向量入口到main函数的调用过程。将被所有的SAMD21工程引用。\mdk\linker\samd21g17d_flash.scf
文件。这是Keil的链接命令文件,定义在build过程中链接的内存空间,其中也定义了芯片中堆和栈空间的大小。默认将被所有的SAMD21工程引用。hal_device_registers.h
文件。这个文件内部包含了具体芯片相关的所有头文件。arm-mcu-sdk中不同芯片的目录中都有这样一个同名的文件,统一使用这个文件抽象对具体芯片寄存器的映射。
/* hal_device_registers.h */
#ifndef __HAL_DEVICE_REGISTERS_H__
#define __HAL_DEVICE_REGISTERS_H__
#include <stdint.h>
#include "samd21g17d.h"
#include "system_samd21g17d.h"
#endif /* __HAL_DEVICE_REGISTERS_H__*/
drivers
目录下包含了一些抽下程度不高的专属于具体芯片的特定驱动,例如,目前管理时钟系统的GCLK
模块,管理电源系统的PM
模块,以及芯片系统集成中的一些可选项的配置SYSCTRL
模块等。
适配yaml generator
arm-mcu-sdk
使用yaml generator
工具,自动化创建工程组织文件,其中包含了对共享源码的引用,确保在调试过程中修改共用代码后可以自动同步到所有引用的工程。在yaml generator
的yaml数据库中添加关于SAMD21芯片和nano-samd21
开发板的记录,即可快速完成适配。
在arm-mcu-sdk\tools\generator\yaml\devices
目录下创建SAMD21.yaml
文件,描述SAMD21微控制器内部集成的外设驱动组件。截至目前,已经完成开发的驱动包括,port
、sercom_usart
、usart_spi
,关于时钟相关模块的驱动,是跟随具体料号的,不具备通用性,故不在此处列出。随着后续开发完成更多的驱动组件,这个清单也会越来越长。
# SAMD21G17D
SAMD21G17D:
drivers:
- sam_common_0
- sam_port_0
- sam_sercom_usart_0
- sam_sercom_spi_0
在arm-mcu-sdk\tools\generator\yaml\boards
目录下创建nano-samd21.yaml
文件,描述了nano-samd21
开发板的定义,其中描述了使用SAMD21G17D
作为主控芯片,并包含的driver_examples
和demo_apps
等样例工程的清单。随着后续开发完成更多的样例工程,这个清单也会越来越长。
devices:
superset: SAMD21
partno: SAMD21G17D
applications:
driver_examples:
sam_port_0:
port_gpio_basic:
port_gpio_input:
sam_sercom_spi_0:
sercom_spi_master_basic:
sam_sercom_usart_0:
sercom_usart_basic:
demo_apps:
basic:
hello_world:
systick_basic:
blinky_led_systick:
defines:
- BRD_NANO_SAMD21
在arm-mcu-sdk\tools\generator\project_template
目录中,创建SAMD21G17D
目录,在其中保存使用Keil新建的、指定使用SAMD21设备的空工程文件project.uvprojx
。在后续使用generator创建各样例工程时,将使用此处的工程文件模板,根据需要包含必要的源文件,以及源文件的包含路径等信息。
最后,在arm-mcu-sdk\tools
目录下创建python文件m_gen_create_nano_samd21_hello_world.py
,编写脚本,运行后即可在arm-mcu-sdk
中创建样例工程。
from generator.prjgen import PrjGen
from generator.prjbuilder import PrjBuilder
import os
def main():
prjgen = PrjGen('mdk', 'nano-samd21')
# prjbuilder = PrjBuilder('mdk')
prjname = os.path.join('driver_examples', 'sam_port_0', 'port_gpio_basic')
newprjpath = prjgen.create_oneprj(prjname)
print(newprjpath)
#buildlog = prjbuilder.build(newprjpath)
#print(buildlog)
print('done')
if __name__ == '__main__':
main()
第一个工程gpio_basic
这里仅以gpio_basic
为例,简要说明arm-mcu-sdk
生成工程的源码结构。
实际上,对于微控制器的开发,实现hello_world的已经是需要相当工作量的工程了,其中涉及到配置合适的时钟源、创建和调用UART驱动,以及对stdio库的适配。相比较而言,使用gpio的输出功能点亮一盏LED灯,配合CPU的空循环实现延时,最终实现闪烁小灯的效果,这样的工程更适合作为上手调试一块微控制器开发板的第一个工程。
图x arm-mcu-sdk工程的源码组织结构
其中,代码功能自底向上分层组织:
- device目录下是芯片相关的最底层抽象,包含启动代码和芯片头文件等。开发者不需要编辑。
- drivers目录是外设驱动程序。开发者不需要编辑。
- board目录下是使用微控制器芯片的电路板相关的配置,以及定制化芯片工作环境的配置代码,例如初始化时钟、配置引脚,以及部分同电路板绑定的外部电路等。
- application目录下存放的是开发应用项目的顶级逻辑,原则上与具体硬件无关。其中的main.c文件中的main()函数,是整个应用程序的入口。
调试外设模块
在arm-mcu-sdk
中为新芯片开发驱动和样例工程遵循一个按需开发的先后顺序,最先开发最基础的驱动,然后逐渐建立依赖关系及样例工程。
port驱动
port驱动中包含gpio功能,而gpio通常是最简单的外设模块。开发者可以利用gpio功能验证程序执行流程的正确性,也可以简单测量芯片到电路板外围电路的连通情况。
sam_port_0
驱动的API清单有:
void PORT_InitPin(PORT_Type * base, uint32_t port_id, uint32_t pin_id, PORT_PinConf_Type * init);
void PORT_SetPinDirection(PORT_Type * base, uint32_t port_id, uint32_t pin_mask, bool output);
void PORT_SetPinOutputLevel(PORT_Type * base, uint32_t port_id, uint32_t pin_mask, bool high);
void PORT_TogglePinOutputLevel(PORT_Type * base, uint32_t port_id, uint32_t pin_mask);
uint32_t PORT_GetPinInputLevel(PORT_Type * base, uint32_t port_id);
void PORT_SetPinMuxR(PORT_Type * base, uint32_t port_id, uint32_t pin_id, PORT_PinMux_Type alt);
对应的单元样例工程有:
driver_examples/sam_port_0/port_gpio_basic
- 使用CPU延时,通过gpio驱动开发板上的LED小灯闪烁。driver_examples/sam_port_0/port_gpio_input
- 捕获开发板上按键的输入状态,当检测按键引脚的电平为底,则输出低电平至LED小灯引脚,点亮小灯,反之熄灭小灯。
SysTick定时器
SysTick是arm内核中自带的定时器模块,在core_cm0plus.h
文件中有定义SysTick_Config()
函数,不需要专门开发驱动。
适配SysTick定时器样例的意义在于,验证使用新芯片的中断系统是否正常工作,并可用于粗略验证主要时钟源的频率是否按照预期工作。SAMD21的SysTick使用AHB总线时钟作为计数时钟源。
对应的样例工程:
demo_apps/basic/systick_basic
- 配置SysTick每1ms产生一次中断,在SysTick的中断服务程序里,使用全局变量tick_counter
对中断次数进行计数,每500次计数,控制翻转一次控制LED小灯的电平。当程序运行时,可以看到LED小灯以1s为周期闪烁。
sercom_usart驱动及样例工程
开发UART驱动的意义在于,可以初步建立MCU同PC的通信,可以适配printf()
。更进一步,可以通过UART的波特率,进一步佐证系统时钟源的准确性。UART驱动的常用配置参数大多相同,重点要关注一下波特率的计算公式。另外,如果激活了UART引擎,还可以适配arm-mcu-sdk中使用循环缓冲区rbuf的样例工程,以及解析AT命令的组件。
sam_sercom_usart_0
驱动的API清单有:
void USART_Init(SERCOM_Type * base, USART_Init_Type * init);
uint32_t USART_GetStatusFlags(SERCOM_Type * base);
void USART_ClearStatusFlags(SERCOM_Type * base, uint32_t flags);
void USART_EnableInterrupts(SERCOM_Type * base, uint32_t flags);
void USART_DisableInterrupts(SERCOM_Type * base, uint32_t flags);
void USART_PutData(SERCOM_Type * base, uint8_t dat);
uint8_t USART_GetData(SERCOM_Type * base);
相关样例工程:
driver_examples/sam_sercom_usart_0/sercom_usart_basic
- 使用轮询方式通过UART引擎向PC及发送“hello_world”字符串,然后使用轮询方式接收UART上的数据并回发。通过这个工程可以验证波特率的计算是否生效,从而佐证时钟源的准确度。实际上,因为DFLL48M时钟源未校准的原因,这里的波特率时钟不对。这个工程也被用于调试波特率。driver_examples/sam_sercom_usart_0/sercom_uart_rx_interrupt
- 使用中断方式接收PC通过UART总线送过来的数据流,存入rbuf的存储对象中,然后再主循环中从接收数据的rbuf中取数并轮询发送回PC机。TBD。demo_apps/basic/hello_world
- 使用轮询收发的方式,适配stdio库,实现printf()
函数。这也是最简单的同PC建立交互的开始。
sercom_spi驱动及样例工程
开发SPI驱动的意义在于,可以驱动开发板上的WS2812B彩灯。是的,nano-samd21-evb板子上的流水灯是一组ws2812b,而不是普通的LED小灯,所以笔者哪怕想写个流水灯的小程序吸引一下在旁边捣乱的小姑娘,也得先把SPI驱动搞出来。原本计划使用这组ws2812b的彩灯显示手指在触摸板的位置,也会用到SPI的驱动。
但是,由于sercom_spi始终无法从近似48MHz的时钟源上整除得到一个6.4MHz的波特率,所以总是控制不好ws2812b小灯的颜色。比价可惜开发板上没有设计SPI接口的Flash存储芯片,否则也可以实现一个LittleFS文件系统的样例。
sam_sercom_spi_0
驱动的API清单有:
void SPI_InitMaster(SERCOM_Type * base, SPI_MasterInit_Type * init);
uint32_t SPI_GetStatusFlags(SERCOM_Type * base);
void SPI_ClearStatusFlags(SERCOM_Type * base, uint32_t flags);
void SPI_EnableInterrupts(SERCOM_Type * base, uint32_t flags);
void SPI_DisableInterrupts(SERCOM_Type * base, uint32_t flags);
void SPI_PutData(SERCOM_Type * base, uint8_t dat);
uint8_t SPI_GetData(SERCOM_Type * base);
相关的样例工程:
driver_examples/sam_sercom_spi_0/sercom_spi_master_basic
- 通过SPI Master以6.4MHz波特率连续发送命令码,点亮并控制WS2812B小灯的颜色。
开发过程中遇到的问题
在开发过程中,笔者遇到了不少问题,由于资料和时间有限,并未进一步深究,但确实影响到了开发体验。也一并在此记录。
开发板上没有预留复位按键
nano-samd21板子上倒是有个按键,但这个按键是个用户按键,用来检测GPIO引脚电平的。没有复位按键的直接后果,就是在调试过程中要恢复现场重新运行程序时,都不得不通过Keil的在线联调功能进行复位。Keil的软复位和RESET引脚的硬复位的行为还是一些区别的。
当我的板子因为非法访问校准数据的NVM产生锁死时,也没有办法试图在硬复位开始,未执行程序的瞬间介入,通过重新刷写程序的方式修复芯片。
开发板上未焊接外部晶振
从SAMD21的手册中可以看到,使用外部晶振和PLL倍频可以很容易地产生非常准的时钟源。但是,nano-samd21板子上没有任何外部晶振,预留了一个32K低速晶振的焊接位但DNP了,压根就没有为高速晶振预留焊接位。
使用芯片内部的DFLL48M振荡器非常麻烦,大体是需要先启用内部的8MHz时钟源,然后在GCLK中绕一个大圈,指定这个8MHz时钟源作为DFLL48M的时钟源,然后再将DFLL48M输出的时钟源再送入GCLK。手册对这个操作过程的描述比较隐晦,我在寒假在家缺乏测量仪器的环境下,调试这个过程始终惴惴不安。使用DFLL48M还需要手动填写Trim值,这个操作在手册上更是描述得不清不楚,我在多次尝试下,触发了隐藏的错误机制,锁死芯片不能下载。
锁死芯片无法重新下载程序
在本文中描述了多次,我为了得到相对准确的DFLL48M时钟源,在从NVM搬运Trim值时,不小心触发了隐藏的错误机制,导致芯片锁死不能下载。我曾翻遍手册尝试找一种bootrom或者ISP的方式,想让芯片在执行flash中的程序之前,先引导到一个安全的工作状态,然后通过刷写未报错的工程,把芯片修回来。然而,我并未找到SAMD21有类似的机制。不得已在春节假期之后又借了一块板子继续完成手头上未完成的开发任务,但已然投鼠忌器。
如此,后续再调试这个Trim功能的时候,就要特别小心再把芯片搞死。要么活动之后可以借鉴下别人的代码,要么以后换用带外部晶振的板子,总之,在找到修复办法之前,我是不敢再动这块内容,否则就没有板子可以再试错了。
总结与展望
通过本次活动,我实际了解了Microchip SAMD21微控制器开发生态,深入读了SAMD21的芯片手册,了解了Microchip技术体系中部分外设模块的设计和应用模型。在实践中,我以芯片原厂系统工程团队的方式对一款新的MCU产品进行了早期开发,创建了对Keil开发工具链的支持,以及对一个成熟的SDK代码库(arm-mcu-sdk)的适配,为后续开发奠定了基础,也为熟悉Keil开发的用户们提供了一种熟悉和便捷的开放方式。
虽然在开发过程中遇到了一些问题,但我仍完成了部分工作。基于现有开发完的驱动和样例工程,已经可以到达到替换传统51单片机应用的程度,可以利用硬件的UART模块同PC机建立通信,利用GPIO模拟通信时序,以及定时器,能够开发一些简单的小项目。
一些遗憾,原本因为Touch功能想试用SAMD21的开发,在活动期间,未能触及Touch相关的开发工作。从技术交流群中了解到,SAMD21的Touch控制器外设,其寄存器访问接口也未在手册中展现,据说是封装成了软件库文件,需要在MCC工具中引用。封装成库必然对原有SDK的代码有依赖,不一定能适配到全开源的arm-mcu-sdk中。。。
在后续的学习过程中,可以继续阅读手册,研读更多外设模块的章节,了解其中的工作机制和用法(架构与建模),并在arm-mcu-sdk的代码仓库中,为SAMD21添加更多的驱动程序和样例工程。在具备必要的技术条件后,尝试开发一些有趣的应用案例,例如,把触摸滑条的功能实现出来,做一点简单的手势识别的应用案例。