IDE:CCS 12.8.1 CLion 2024
DSP:TMS320VC5509APGE
虽然现在TI已经不再主动更新C55x系列,而是转为支持C6000等。但某些情况下仍需要使用C55x的芯片学习,在旧文档里可能提到可以下载最新的CCS。不过实际上,CCS20已经不再支持C55x系列了,只有CCS12及以前的版本支持,CCS 12.8.1算是最后一个支持C55x系列的IDE了。
由于TI的调试器什么的都是TI自研,所以其他IDE就很难越俎代庖了。不过即便如此,其他IDE仍可发挥巨大的作用。比如此处,CLion凭借简洁优美的界面、强大的代码补全等功能,即使只用作代码编辑器已是方便了许多。
CLion的安装与使用可参考博客:
一、CCS下载
找到官网,下载对应的版本
CCSTUDIO IDE、配置、编译器或调试器 | 德州仪器 TI.com.cn
前面的安装过程如常,注意安装路径不要有中文和空格
如果是自定义安装,那么至少要勾选C55x。如果存储足够的话,可以选择全部安装(避免后期缺乏一些依赖)
安装完成后记得重启。
汉化可以参考博客CCS12.0 安装并设置中文_xuejianqiang的技术博客_51CTO博客
语言包可以选择这个Eclipse software repository | The Eclipse Foundation
芯片支持库
关于这个芯片的开发资料太少了,包括官网。在CCS的资源搜索里只能搜索到下面两个简单的的指导文档类型的东西,并且还是白屏。实体书上的内容倒是很详细,不过绝大多数都是介绍芯片的架构。
通过强制搜索C55x也仅仅找到一个看起来像的软件支持包
在TI官网里搜索可以看到芯片支持库
注意!这里一共提供了两个包,上面这个带SPRC133的才包含了C5509A的软件支持包,不注意非常容易搞错!(本人在数据手册与芯片支持库严重不符之间徘徊犹豫了许久,才猛然发现库下错了)
这是C5509A的正确的下载链接:https://dr-download.ti.com/software-development/driver-or-library/MD-0MVQS9HBZ7/01.00.00.00/sprc133.zip
下载后解压,就是这样的一个程序。嗯,05年的,有且只有1.0一个版本
运行后
安装路径选择一条合适的并且没有中文空格的
其他库
在TI官网上搜索C55x,得到的信息也极为有限,不过这里还是有DSP库、图形库和芯片驱动库。
C55x编译器 (CGT)
这是各种编译器的下载链接:TI编译器_链接
这是C55x编译器的链接:C55x编译器_链接,网站挺一把年纪的
正常情况下,进入这个网站会一直转圈。科学上网后,仍需要一些时间才能加载。
奇怪的是v4.4.2只有Mac版本,先下载v4.4.1再说
下载后会让你填一张表,使用目的填民用即可,然后接收协议。
然后就可以得到指定编译器,这个是编译器的使用文档。TMS320C55x Optimizing C and C++ Compiler User's Guide
由于芯片是TI自研的C55x DSP架构,GCC里并没有它的交叉编译器
安装
安装过程很简单,先接受协议
选择典型即可
然后放到一个合适的目录,出于习惯,路径上没有中文和空格
C5500_Code_Generation_Tools4.4.1
最后等待安装完成即可
ReadMe里有一堆关于编译器的说明,比如允许在C++代码中使用GNU C++支持的GCC扩展语法、ANSI模式扩展、新增GCC特性适配(如always_inline、noreturn、malloc等)、内置函数扩展(新增编译时常量判断__builtin_constant_p和栈帧地址获取__builtin_frame_address)等等
说明
网上有一些关于C55x的代码和汇编怎么写,但是并没有多少资料介绍C55x编译器怎么使用,建议还是根据这个指南来
另外有些很优秀的博客可以参考:
TMS320C55x之C/C++语言程序设计_.switch段-CSDN博客
总得来说,作为一个多年前就停产的编译器,在语法方面还是很古老的。在C方面,它支持C89标准和一些GCC上的扩展,但是不支持C99。在C++方面,仅支持C++98,不过命名空间、类、重载等常用的特性都是有的。当然,没有auto
CCS20无法识别编译器(旧)
安装后,一般来说可以在CCS里找到编译器选项。点击Project,再点击Properties
如果找不到,那么可以手动添加。点击设置
在这里把之前安装编译器的路径填上去(不要用中文空格什么的),但是它无法识别出来。因为CCS 20不再支持C55x系列芯片了
二、创建工程
1,空白工程创建
创建工程时,选择对应的目标和芯片型号,然后起一个项目名称名。然后会提示没有编译器,点击【More...】
然后会弹出一个界面,在上面可以添加前面安装编译器的路径,它会自动识别(CCS 20识别不出来)。
如下图,C5500即为我们需要的编译器
点击应用关闭后,编译器会自行配置。项目就选择默认的带main.c的空项目
创建完成后,如下:
点击小锤子,即可编译。编译后的内存分布,真别说,还挺详细的。
根据这个控制台输出提示,可以看出它使用的是gmake管理项目,然后巴拉巴拉一堆配置,这为我们后面使用CLion编译项目提供了重要信息。
2,项目简单分析
初创建的工程,可以看到没有资源管理器,我们可以在View — Project Explorer中打开
然后稍微调整一下布局,即可得到一个熟悉的开发环境
打开VC5509.cmd,也就是“链接脚本”,可以看到内存布局的定义和段定义。没有特殊需求的情况下,是不用改的。可以看到代码段和存储变量的段所用的内存类型是不同的,一个是SARAM,另一个是DARAM
接下来可以看到这个makefile文件,这为我们后来在CLion上开发工程提供了极为有力的支撑。
在子Makefile里,可以看到各种资源文件的定义,不过这些可以使用图形化添加的方式来自动更新。
在Debug目录里,也就是构建目录,可以看到map文件,把各个链接符号都映射出来了。
3,导入片上支持库(不推荐)
是否导入这个库需要仔细权衡,该库并不显式开放源码,并且只支持小存储器模式。想要获取源码可以参考目录三。
不推荐的理由也很简单,库是2005年第一版且只有一版,源码甚至是2000年,与最新的工具链v4.4.1(2013年)有不少不兼容的地方,比如汇编语法。不过静态库lib还是能用的,只是源码初次无法编译。
此外,如果以前使用过STM32的标准库或HAL的话,那么使用这个CSL就会有许多别扭的地方,虽然有官方示例,但示例太少,连GPIO的示例都没有,而且API还相当难用,不好理解实现细节。自己根据手册编写自己的函数或许是更好的方案。
可以直接把支持包里的inc和lib文件夹复制到当前工作环境(此时工作空间是new_demo,工程名为demo),也可以为其创建一个C55xxSL父目录
文件夹添加之后,CCS里就会自动加载。不过构建时,会报错,这是因为头文件并没有被识别到,即include目录没有被添加。(下图的inc是以前遗留的痕迹)
添加头文件目录
先点击左边资源管理器的项目名称,然后再点击Project
接着点击属性,再找到Include Options
把光标放在上面可以看到具体的目录,这里可以看到PROJECT_ROOT这个变量代表的就是我们的工程目录demo
因此我们可以根据相关变量来添加我们驱动库的头文件路径。点击旁边很小的添加图标
点击变量,然后找到WORKSPACE_LOC这个变量(因为我们的库是放在工作环境里的,而非项目目录),并点击确定
然后输入对应的相对目录
添加编译宏
添加头文件目录后编译一切正常,在下面的输出提示可以看到,库并没有被包含进去,因为我们这个main里并没有引用库里的任何东西。
引用后,给了惊喜
根据错误提示,可以定位到这一行。可以看出是缺少芯片定义,这与当初的stm32的HAL库一样,需要添加一个编译宏或者在一个文件里添加这个宏。
从前面的代码可以看出,只要定义这个CHIP_5509A的宏就可以了
从编译器里可以看到,是有这个宏的,只不过这两个宏不一样。不过这也很正常,芯片支持包是05年的,编译器是15年的
为了防止改了后出问题,这里我们选择再添加一个宏CHIP_5509A
--define=CHIP_5509A
这下编译就不报错了。可以看到提示因为没有链接其他库,因为我们只是引入头文件,但是并没有调用里面的函数。
添加库目录
接下来随便使用里面的函数
可以看到构建时会报错,因为没有添加库的目录
与前面添加头文件目录一样,找到属性界面,再选中如图选项
在lib目录里看看有没有自己型号的库,本篇以C5509A为例
在上面填上需要的库csl5509a.lib,在下面,把lib目录给添加上去
意外の链接缺失
最后没想到还是会报错,无法识别链接符号??
为此,只能使用TI的工具来解压库,看看里面有没有对应的链接符号
解压后可以得到一堆obj文件
我们所需要的应该在gpio_enab.obj里面,所以需要把nm55这个工具复制过来,然后输入命令查找
解出来好几个obj文件,发现里面啥都没有,这还咋玩
后来才发现前面是因为没使用对头文件,应根据宏定义来判断头文件间的依赖关系
也就是说使用csl_前缀的头文件即可,不要用_csl_等前缀的函数,因为csl里面的条件预编译会自动包含对应的头文件,处理依赖关系。
【重点】切换存储模式
现在是可以使用函数了,但是需要small存储模式,这是因为创建工程时默认是large。
C55x一共有small、large和huge三种存储器模式
在这里改成小存储模式,上面会提示错误
点击左边的选项,再点击右下角,修改编译器的标志
如此一来可以正常使用片上支持库
4,编码
如果文件乱码,可以在首选项里更改编码。一般来说,国内的老工程一般是GBK编码(毕竟更适合中文环境),现代IDE和国外的库一般默认是UTF-8编码(更适合英文环境)。至于如何取舍,根据需求即可,只要保证项目都为同一个编码即可,反正IDE都是可以设置编码的。
如果想要从GBK编码转为UTF-8编码,有许多工具可以使用,基本原理很简单:先以GBK的方式加载,再以UTF-8的方式写入。现代IDE一般是自带编码转换,比如VS Code(需要安装插件)、CLion等
三、CSL库的简单使用
1,说明文档
下载库时,首先要看说明文档和示例。如图所示,这个说明文档时htm类型,当成网页打开即可
从这个说明文档了获得了一个此前从未发现的重要信息:
即想要获取源码可以执行相应的解压命令
2,获取源码
解压
我们先创建一个目录source,用于存放解压的源码文件
并且把csl55xx.src复制进去
找到工具链的路径,把ar55.exe这个工具放入source目录里
在当前目录下打开终端,输入前面看到的命令
./ar55 x csl55xx.src
可以看到,各种源文件就被解压出来了,不用考虑该死的存储模式限制了
整理
直接使用源码显然更方便,因为这个库只发布了一版,必然有不完善的地方和bug,有了源码可自行调整或者修复。
前面解压的文件是.h与.c混合,我们可以在外面创建一个inc和src目录
在解压后的source目录下,把所有头文件全部剪切到inc目录里
再把所有.c剪切到src目录
最后还剩下两个汇编文件,也剪切到src里或者新建一个asm目录
3,编译源码(未验证,不建议使用)
创建目录
接下来我们可以自行创建目录把刚才获得的inc、src目录(和asm目录,如果有的话)放进去,此处模仿STM工程,创建的是Core目录,里面再创建一个C55xxCSL目录。
图中的Drivers目录为遗留目录,不用考虑
更新项目配置
既然使用源码不使用编译好的库,那么我们需要把之前添加的库目录删掉,然后添加头文件目录。那么应剩下的头文件目录为下图蓝色区域的,其他头文件目录是我整理工程时创建的,与前面教程无关。
链接库和库目录应如下
编译
在main里还是如前面一样,引入一个头文件和并调用里面的函数。重新编译后会发现报错
跟随报错,可以看到一系列汇编报错。嗯,又是汇编,又是汇编语法错误。库与编译器的年龄相差十年有余,不兼容也很正常
先把它俩删除再重新构建
重定义问题
可以看到,报了下面错,其一是重定义问题
通过观察代码,感觉是csl_sysdata.c里的定义段好像漏了一个a,或许这里面的代码可以先注释掉。
为了便于后续对照,这里先存个档(如果有git的话或者别的什么手段)
接下来再尝试解决未定义符号的问题,显然与我们之前删除的汇编文件有关
新建asm目录,再从解压源码的库里把asm文件复制过来
汇编问题
添加汇编文件后,我们再次尝试编译项目,发现能看到的错误基本都是语法错误
不过我并不清楚编译这个汇编文件的是哪种编译器,是汇编器还是C/C++编译器?
那么可以到Debug构建目录里查看对应的子makefile文件,从这里可以看出,编译汇编文件的是cl55。
汇编这块与cl55的汇编语法相差太大,很难搞,用AI搞了许久编译才没有报错,但代码未经验证,仅供参考
irq_pluga.asm
;\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ ; CSL中断向量安装函数二次修正版 ; 主要修正点: ; 1. 修正PSH/POP指令语法 ; 2. 符合CPL=1模式的堆栈操作 ; 3. 优化条件判断逻辑 ;\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ .asg 123, CSL_DATA_PTR .asg 05E80h, NOP16_Operator .asg 0, VecLoc .asg 2, IsrAddr .asg 4, Iptr .asg 5, EventBit .asg 6, EventMask .asg 7, EventId .asg 8, Old_INTM .asg 9, LocalFrameSz .asg 12, BiosPresentFlag .asg 049h, IVPD_ADDR .asg 04Ah, IVPH_ADDR .asg 001h, IFR0_ADDR .asg 046h, IFR1_ADDR .mmregs .def _IRQ_plug .ref _IRQ_globalDisable, _IRQ_globalRestore .cpl_on ; 设置CPL模式 _IRQ_plug: PSHBOTH XAR3 PSHBOTH XAR2 AADD #-LocalFrameSz, SP MOV AC0, dbl(*SP(#IsrAddr)) MOV T0, *SP(#EventId) MOV #0, AC0 MOV AC0, XAR2 MOV AC0, XAR3 CALL _IRQ_globalDisable MOV T0, *SP(#Old_INTM) MOV #IVPD_ADDR, AR2 ; 使用MOV加载地址到AR2 MOV *SP(#EventId), AC1 AND #0x18, AC1, AC3 BTST @#4, AC3, TC1 ; 使用BTST测试第4位(0x10) BCC L_IVPH, TC1 MOV #IVPH_ADDR, AR2 ; 使用MOV加载IVPH地址 L_IVPH: SFTS AC1, #3, AC1 MOV *AR2, AC0 SFTS AC0, #8, AC0 OR AC1, AC0 SFTS AC0, #-1 MOV AC0, dbl(*SP(#VecLoc)) MOV dbl(*SP(#IsrAddr)), AC0 MOV dbl(*SP(#VecLoc)), XAR3 ; 使用MOV加载扩展地址 MOV *SP(#EventId), AR1 AND #0x0F, AR1, AR2 MOV AC0, dbl(*AR3) MOV #NOP16_Operator, AC3 ADD #2, AR3 MOV AC3, *AR3+ MOV AC3, *AR3+ MOV AR2, T0 AND #0x10, AR1 MOV #1, AC0 SFTS AC0, T0, AC0 BTST @#4, AR1, TC1 ; 检测bit4 ; 修正为C55x标准条件码 BCC L_IFR0, !TC1 ; 使用逻辑非操作符+TC状态位 MOV #IFR1_ADDR, AR2 MOV AC0, *AR2 B RESTORE_INTM L_IFR0: MOV #IFR0_ADDR, AR2 MOV AC0, *AR2 RESTORE_INTM: MOV *SP(#Old_INTM), T0 CALL _IRQ_globalRestore EPILOGUE: AADD #LocalFrameSz, SP POPBOTH XAR2 POPBOTH XAR3 RET
usb_vect.asm
;********************************************************************* ;** api_vect.s55 - TMS320C55x USB API向量表(100%验证通过版本) ** ;********************************************************************* ;**************************** 常量定义 ****************************** USBIDLCTL .set 0x7000 ; USB空闲控制寄存器地址 USBIDLCTL_USBEN .set 0x0004 ; USB使能位掩码 USB_API_PTR .set 0x667E ; API向量表指针地址 ;*************************** 全局符号声明 *************************** .def _USB_setAPIVectorAddress .ref _vUSB_getEvents, _vUSB_peekEvents, _vUSB_getSetupPacket ;*************************** API向量表段 *************************** .sect "api_vect" .align 256 _ROM_API_TABLE: .word 0x0000, _vUSB_getEvents ; 24位地址存储格式 .word 0x0000, _vUSB_peekEvents .word 0x0000, _vUSB_getSetupPacket .space 28 * 32 ; 保留空间 ;*************************** API查找表段 *************************** .sect "api_lut" _USB_getEvents: PSHBOTH XAR5 AMOV #_ROM_API_TABLE, XAR5 ; 加载24位地址 MOV dbl(*AR5), AC0 ; 读取完整24位地址 CALL AC0 ; 合法调用语法 POPBOTH XAR5 RET _USB_peekEvents: PSHBOTH XAR5 AMOV #_ROM_API_TABLE, XAR5 ADD #4, AR5 ; 每个条目占4字节 MOV dbl(*AR5), AC0 CALL AC0 POPBOTH XAR5 RET _USB_getSetupPacket: PSHBOTH XAR5 AMOV #_ROM_API_TABLE, XAR5 ADD #8, AR5 MOV dbl(*AR5), AC0 CALL AC0 POPBOTH XAR5 RET ;*************************** 初始化函数 *************************** .sect ".text" _USB_setAPIVectorAddress: PSHBOTH XAR1 MOV #USBIDLCTL, AR1 OR #USBIDLCTL_USBEN, *AR1 MOV #((_ROM_API_TABLE >> 16) & 0xFF), AR1 ; 高8位地址 MOV AR1, *(USB_API_PTR) MOV #(_ROM_API_TABLE & 0xFFFF), AR1 ; 低16位地址 MOV AR1, *(USB_API_PTR+1) POPBOTH XAR1 RET .end
函数未定义
解决了汇编问题后又暴露下面错,缺失_RTC_asctime符号,也就是说RTC_asctime函数没有定义,但事实上已经在rtc_atime.c里定义过了
进入这个文件,发现没有通过预编译
和另一个文件比较,发现前面那个#include <csl_chiphal.h>放错位置了
应把csl_chiphal.h放在上面
编译效果
编译效果十分夸张,代码占用远超前面使用静态库的结果,并且编译极其费时,且占用了不少内存。前面也能看到,头文件放错位置、变量定义了两遍这些基础错误,按理来说,源码应与静态库对应,但实际上静态库配置好的话一次就能很快通过编译,源码则是各种错误不断。
不过除了那个头文件位置和变量重定义错误外,其他的问题应该是语法“断代”了,只有eabi还兼容
那么现在使用csl库只有两个选择,一种是直接使用提供的静态库,省时省力不知细节,但只能使用小存储器模式;另一种是使用源码编译,费时费力知晓细节。事实上从前面出现的那么多错误的情况下,我不太敢用,不过还是有参考意义的。
4,官方示例
在之前的库目录里,可以看到示例里有三个不同版本,这里我们使用5509那个
里面有一些外设的示例,不过并没有GPIO的
从I2C这个示例可以看出,如果想要使用CSL库函数,那么在main函数的第一行就应该调用CSL_init(),这点与使用HAL时的HAL_Init()类似
不过细看main函数和taskFunc函数,感觉有些奇怪,没有死循环!
可能官方只是抛出一个示范而已,没有贴合实际开发场景?
5,调试
CCS的调试设置简直就是个奇葩,因为它的断点默认是全部跳过。需要你在【运行】工具栏里找到下面选项,点击即可,确保设置的断点没有一条斜线。
断点的功能是很丰富的
右键断点,会有个断点属性
进去后可以设置各种事件
同时优化等级也会影响断点调试功能,有些简单函数会被接被内联展开,反映在调试上就是无法设置断点。解决办法就是把优化等级改为静止优化(平时一般开O2)
四、C55x C/C++编译器的使用 【待更新】
cl55,也就是C55x系列的C/C++编译器
1,编译器优化【待更新】
删除未使用的函数,GCC只需要向链接器传入标志-ffunction-sections【待更新】
2,编译器扩展
GCC拓展
cl55除了自身的扩展外,它还支持大量GCC扩展
GCC扩展列表如下(灰色背景的表明该编译器不支持):
启用GCC扩展
想要使用GCC拓展,那么就需要添加--relaxed_ansi或者--gcc标志
在CCS里,即勾选下面选项
勾选后,在这个列表框里是--relaxed_ansi标志
实际上两个标志都传进去了
3,语法
内联
cl55的内联只有在开启了优化后才能使用,不过以一般理性而言,绝大多数情况都是会开启编译优化的。
在头文件里,理论上可以使用强制内联,但我在实际使用过程中,发现似乎没有生效,无论是使用gcc扩展的强制内联还是cl55自带的扩展。
建议还是使用static inline,可以保证最大的兼容性。
4,数据类型
需要格外注意的是,cl55里并没有我们平时意义上的uint8_t类型,最低只有16位。其中,小存储器模式下,指针的长度为16位,而大存储器模式下,指针的长度为23位
也就是说,在C55x中,一字节就是16位,而非8位!
5,忽略警告
有些时候,cl55编译文件时经常会出现一些奇奇怪怪的警告,这些警告在GCC下是没有的,cl55有些反应过度了,比如每个文件的最后一行必须是空行、不能随便给一个常量强制映射为寄存器。
这时候有两种办法可以忽略警告,一种是在文件内通过#pragma向编译器传参,另一种是直接给编译器添加标志
使用#pragam传参
可以通过这两个预编译命令忽略中间内容的特定警告
#pragma DIAG_SUPPRESS <编号> // 压制指定警告 /*...*/ #pragma DIAG_DEFAULT <编号> // 恢复默认处理
使用编译器标志 (推荐)
打开项目属性可以看到这两个参数,即忽略1和173号警告
也可以展开编译器选项,在相应位置填写要忽略的编号。一般来说可以忽略1、173警告
6,注意事项
段错误(内存错误)
使用cl55编译器编译文件时,有时会因为编译过程中段的大小不够而导致错误。下面是编译u8g2_fonts.c时报的错误,显然,常量段不够
解决办法很简单,要么减少常量的定义,要么增大常量段的大小。不够后者并不一定能生效,即使分配了足额的存储空间,仍会出现问题,可能是编译器本身的限制。
五、CC5509A芯片
1,时钟系统
介绍
从框图中可以看出C5009A的时钟系统还是很简单的,只有一个数字锁相环DPLL,它用于为CPU等提供时钟。注意不要把它与旁边的USBPLL搞混了,这是很容易搞混的,因为SYSR(系统寄存器,用于配置输出时钟的分频系数)下面,就紧跟者USBPLL
唯一的问题是,数据手册里并没有介绍配置DPLL的时钟模式寄存器CLKMD,并且网上已经没有任何关于这款芯片的CLKMD的介绍了,尤其是TI官网已经把这款芯片的DPLL手册删除了,链接www-s.ti.com已经无法访问了。
只有一些TMS320C55x的资料书和远古代码上还有这个寄存器的一些信息,不然这个寄存器的使用方法除了蒙,就只有上帝知道了。
DPLL
估计这是网上唯一一份关于CLKMD的信息了。
根据这个手册,可以很轻松地写出相应代码。为了便于观察代码,把里面与USBPLL的配置删除了
2,中断
中断这一块,教材一般会介绍相关寄存器和中断向量表,一般不怎么讲代码怎么写。这部分内容应看C55x编译器指导书。
从这段内容可知,整个程序必须链接一个引导例程,整个引导例程完成下面6个操作:
- 配置状态和控制寄存器
- 初始化主堆栈和次级系统堆栈
- 处理
.cinit
运行时初始化表,自动初始化全局变量(使用--rom_model
选项时)- 调用所有全局对象的构造函数
- 调用
main
函数- 在
main
返回后调用exit
函数
其行迹可以描述为
/* boot.obj伪代码 */ void _c_int00() { init_system_clock(); // 初始化时钟 setup_memory_controller(); // 配置内存控制器 copy_cinit_section(); // 初始化全局变量 call_constructors(); // 调用C++构造函数 main(); // 进入用户程序 exit(); // 程序退出处理 }
在map文件里可以找到相关线索
这是从CSL中的定时器例程上扒来的上古中断向量代码,内蕴上古大能留下的不可言说的玄机。中断服务例程的注册方式遵循早已失传的芯片占位符语法,如今新晋修士面对等内嵌asm volatile等孽物,只得通过调试追寻中断标志位的跳变逻辑。若用交叉引用之法,结合CCS工程里泛黄的注释残片,辅以《TMS320C55x Optimizing C/C++ Compiler v4.4》第 4.3节残缺的运行机制讲解,或可破译其中断嵌套的洪荒之力——须知这类代码实乃嵌入式世界的活化石,唯有考古是出路。
* * Copyright (C) 2003 Texas Instruments Incorporated * All Rights Reserved * * *---------vectors_dma2.s55--------- * * Assembly file to set up interrupt vector table * .sect ".vectors" *------------------------------------------------------------------------------ * Global symbols defined here and exported out of this file *------------------------------------------------------------------------------ .global _VECSTART *------------------------------------------------------------------------------ * Global symbols referenced in this file but defined somewhere else. * Remember that your interrupt service routines need to be referenced here. *------------------------------------------------------------------------------ .ref _c_int00 .def nmi, int0, int1, int2, int3, int4, int5, int6 .def int7, int8, int9, int10, int11, int12, int13 .def int14, int15, int16, int17, int18, int19, int20 .def int21, int22, int23, int24, int25, int26, int27 .def int28, int29 _VECSTART: .ivec _c_int00,use_reta nmi .ivec no_isr nop_16 int0 .ivec no_isr nop_16 int1 .ivec no_isr nop_16 int2 .ivec no_isr nop_16 int3 .ivec no_isr nop_16 int4 .ivec no_isr nop_16 int5 .ivec no_isr nop_16 int6 .ivec no_isr nop_16 int7 .ivec no_isr nop_16 int8 .ivec no_isr nop_16 int9 .ivec no_isr nop_16 int10 .ivec no_isr nop_16 int11 .ivec no_isr nop_16 int12 .ivec no_isr nop_16 int13 .ivec no_isr nop_16 int14 .ivec no_isr nop_16 int15 .ivec no_isr nop_16 int16 .ivec no_isr nop_16 int17 .ivec no_isr nop_16 int18 .ivec no_isr nop_16 int19 .ivec no_isr nop_16 int20 .ivec no_isr nop_16 int21 .ivec no_isr nop_16 int22 .ivec no_isr nop_16 int23 .ivec no_isr nop_16 int24 .ivec no_isr nop_16 int25 .ivec no_isr nop_16 int26 .ivec no_isr nop_16 int27 .ivec no_isr nop_16 int28 .ivec no_isr nop_16 int29 .ivec no_isr nop_16 *------------------------------------------------------------------------------ * This is a dummy interrupt service routine used to initialize the IST. *------------------------------------------------------------------------------ .text .def no_isr no_isr: b #no_isr *------------------------------------------------------------------------------
七、自行搭建驱动库
由于博客越长,写起来越卡,现在翻个页写几句都要响应几秒,只能放在新博客了,后面有空再更新。C++的使用先提一嘴,只要把.c文件改为.cpp文件即可,不需要改.h文件为.hpp文件。不过也可以启用相关标志让编译器把.c文件也当成.cpp文件来处理。
1,框架
出于性能和存储的考虑,没有把函数写成类似于STM32的HAL那样,使用结构体来初始化,而是使用各个独立功能的函数来完成初始化。由于本芯片的片上外设并不复杂,这种方式还能接受。如果是更复杂的片上外设,那么使用结构体更好一些,既清晰,又不容易忘。
由于编译器限制,只能使用C89或者C++98。不过C89使用起来实在不顺手,尤其是写for循环的时候,必须把变量放在前面定义。因此打算使用C++98来搭建整个驱动库。既然使用了C++,那么模板、命名空间、类、重载、引用等特性就要好好利用。
开发驱动库,那么寄存器的映射和相关操作必然少不了,因此可以尝试把寄存器映射这些操作封装为一个类(或者结构体)。zq是整个命名空间的名称,也可自己起相关名称,比如hal。其下的mmio其实就是内存映射操作的,属于zq的子集,往后可以扩充,比如timer、gpio等,这可以很好地利用命名空间的可拓展性。
namespace zq { namespace mmio { template<uint16_t Address> struct RegAccess { // 编译期地址计算 static inline volatile ioport uint16_t *ptr() { return reinterpret_cast<volatile ioport uint16_t *>(Address); } // 单位操作(操作的是掩码) static inline void set_bit(const uint16_t mask) { *ptr() |= mask; } static inline void clear_bit(const uint16_t mask) { *ptr() &= ~mask; } // 读取位 static inline bool read_bit(const uint16_t mask) { return (*ptr() & mask) != 0; } static inline bool read_bit_not(const uint16_t mask) { return (*ptr() & mask) == 0; } /** * * @param value 要修改的值 * @param mask 掩码 * @param shift 位偏移 */ static inline void modify_bits(const uint16_t value, const uint16_t mask, const uint16_t shift) { *ptr() = (*ptr() & ~mask) | ((value << shift) & mask); } // 直接向寄存器写入16位的值 static inline void write(const uint16_t value) { *ptr() = value; } static inline uint16_t read() { return *ptr(); } }; } // namespace mmio } // namespace zq
2,寄存器映射
enum class
接下来,我们再考虑寄存器的定义,在stm32里是使用结构体或者直接使用宏来定义地址的,对于寄存器的位域一般通过掩码或者位移,一般不使用C/C++的位域语法(大小端兼容问题)。不过此处使用C++,我们可以让它以一种更优雅的方式来定义。
我们可以使用enum class来封装寄存器地址等相关信息,不过C++98里并没有enum class,那么我们可以通过struct和enum来模仿它。
比如在zq::timer命名空间里
namespace timer { struct TCR0 { enum { REG = 0x1002 }; }; }
此时我们可以通过TCR0::REG来访问TCR0这个寄存器的地址,而不是通过enum直接暴露来访问,毕竟后面还有许多寄存器,这因为同名而报错。
同理,可以在里面再嵌套一个结构体来包含位域的相关信息
namespace timer { struct TCR { enum { REG = 0x1002 }; // [15] 0:不能进入IDLE状态 1:可以进入IDLE状态,当PERIS为1时进入IDLE状态 struct IDLEEN { enum { MASK = 0x8000, SHIFT = 15 }; }; }; }
此时,我们可以根据这些信息很方便地定义TIM0类
namespace timer { struct TCR0 { enum { REG = 0x1002 }; // [15] 0:不能进入IDLE状态 1:可以进入IDLE状态,当PERIS为1时进入IDLE状态 struct IDLEEN { enum { MASK = 0x8000, SHIFT = 15 }; }; }; class Timer0 { // 寄存器 typedef mmio::RegAccess<TCR0::REG> TCR0_REG; public: // 设置IDLE是否使能 0:禁止进入IDLE状态 1:允许进入IDLE状态 template<uint16_t mode> static void set_idle_mode() { TCR0_REG::modify_bits(mode, TCR0::IDLEEN::MASK, TCR0::IDLEEN::SHIFT); } }; }
使用宏来封装
不过问题也很明显,那就是这样写太占地点了,区区三个信息就能占用好几行,不便于阅读。因此我们可以使用宏来解决
// 直接在寄存器作用域中声明位域 #define DECLARE_BITS_FIELD(BITS_NAME, MASK_VAL, SHIFT_VAL) \ struct BITS_NAME{\ enum { \ MASK = MASK_VAL, \ SHIFT = SHIFT_VAL \ }; \ } // 快速声明整个寄存器(无位域) // 映射寄存器地址,以便供寄存器类使用 可以通过NAME::REG来访问寄存器地址 #define DECLARE_REGISTER(REG_NAME, ADDRESS) \ struct REG_NAME { \ enum { \ REG = ADDRESS \ }; \ }
那么现在声明就好用了许多
namespace timer { // TIM0寄存器 DECLARE_REGISTER(TIM0, 0x1000); struct TCR0 { enum { REG = 0x1002 }; DECLARE_BITS_FIELD(IDLEEN, 0x8000, 15); // [15] 0:不能进入IDLE状态 1:可以进入IDLE状态,当PERIS为1时进入IDLE状态 DECLARE_BITS_FIELD(INTEXT, 0x4000, 14); // [14]标志位:时钟源从内部切换为外部标志 0:外部时钟源没有准备好 1:外部时钟源已经准备好 DECLARE_BITS_FIELD(ERRTIM, 0x2000, 13); // [13]检测错误标志 0: 没有发生错误 1: 发生了错误 DECLARE_BITS_FIELD(FUNC, 0x1800, 11); // [11]工作模式选择位 DECLARE_BITS_FIELD(TLB, 0x0600, 10); // [10]定时器装载位 DECLARE_BITS_FIELD(SOFT, 0x0020, 9); // [9]软件触发位 DECLARE_BITS_FIELD(FREE, 0x0100, 8); // [8]与仿真断点有关 DECLARE_BITS_FIELD(PWID, 0x00C0, 6); // [6]窄脉冲输出宽度 每当TIM归零时,输出指定宽度的窄脉冲 DECLARE_BITS_FIELD(ARB, 0x0010, 5); // [5]自动重装控制位 DECLARE_BITS_FIELD(TSS, 0x0010, 4); // [4]定时器停止状态位 0:启动定时器 1:停止定时器 DECLARE_BITS_FIELD(CP, 0x0008, 3); // [3]定时器输出时钟/脉冲模式选择 0:脉冲模式 1:时钟模式,占空比固定为50% DECLARE_BITS_FIELD(POLAR, 0x0004, 2); // [2]时钟输出极性位 DECLARE_BITS_FIELD(DATOUT, 0x0002, 1); // [1]GPIO模式下,控制引脚输出电平 }; /*…………*/ }
命名空间合并
不过,仍有瑕疵,那就是两个宏不能同时使用,那么就仍有结构体和枚举体的出现。除非这个寄存器像TIM0那样没有位域划分,只需要调用一个宏即可。此外也可能出现寄存器定义与寄存器位域定义不在同一个文件的情况,那么结构体就不能使用了。
那么只有命名空间能出手了,利用其可合并的特性。只需要把struct替换为namespace即可,命名空间可以重名,重名后会自动合并
namespace timer { // TCR0寄存器地址 namespace TCR0 { enum { REG = 0x1002 }; } // TCR0寄存器位域 namespace TCR0 { namespace IDLEEN { enum { MASK = 0x8000, SHIFT = 15 }; } } }
那么可以为它定义这样的宏
// 用于指定寄存器快速声明寄存器的位域 #define DECLARE_BITS_FIELD_FROM_REG(REG_NAME,BITS_NAME, MASK_VAL, SHIFT_VAL) \ namespace REG_NAME {\ namespace BITS_NAME{\ enum { \ MASK = MASK_VAL, \ SHIFT = SHIFT_VAL \ }; \ }\ } // 直接在寄存器作用域中声明位域 #define DECLARE_BITS_FIELD(BITS_NAME, MASK_VAL, SHIFT_VAL) \ namespace BITS_NAME{\ enum { \ MASK = MASK_VAL, \ SHIFT = SHIFT_VAL \ }; \ } // 快速声明整个寄存器(无位域) // 映射寄存器地址,以便供寄存器类使用 可以通过NAME::REG来访问寄存器地址 #define DECLARE_REGISTER(REG_NAME, ADDRESS) \ namespace REG_NAME { \ enum { \ REG = ADDRESS \ }; \ }
那么使用起来就会异常方便清晰,前面的寄存器声明可以放在别的文件里,而这个寄存器位域可以作为后续拓展,随时随地在别的文件里声明。
namespace timer { // TIM0寄存器 DECLARE_REGISTER(TIM0, 0x1000); // TCR0寄存器 DECLARE_REGISTER(TCR0, 0x1002); // TCRO寄存器位域 namespace TCR0 { DECLARE_BITS_FIELD(IDLEEN, 0x8000, 15); // [15] 0:不能进入IDLE状态 1:可以进入IDLE状态,当PERIS为1时进入IDLE状态 DECLARE_BITS_FIELD(INTEXT, 0x4000, 14); // [14]标志位:时钟源从内部切换为外部标志 0:外部时钟源没有准备好 1:外部时钟源已经准备好 DECLARE_BITS_FIELD(ERRTIM, 0x2000, 13); // [13]检测错误标志 0: 没有发生错误 1: 发生了错误 DECLARE_BITS_FIELD(FUNC, 0x1800, 11); // [11]工作模式选择位 DECLARE_BITS_FIELD(TLB, 0x0600, 10); // [10]定时器装载位 DECLARE_BITS_FIELD(SOFT, 0x0020, 9); // [9]软件触发位 DECLARE_BITS_FIELD(FREE, 0x0100, 8); // [8]与仿真断点有关 DECLARE_BITS_FIELD(PWID, 0x00C0, 6); // [6]窄脉冲输出宽度 每当TIM归零时,输出指定宽度的窄脉冲 DECLARE_BITS_FIELD(ARB, 0x0010, 5); // [5]自动重装控制位 DECLARE_BITS_FIELD(TSS, 0x0010, 4); // [4]定时器停止状态位 0:启动定时器 1:停止定时器 DECLARE_BITS_FIELD(CP, 0x0008, 3); // [3]定时器输出时钟/脉冲模式选择 0:脉冲模式 1:时钟模式,占空比固定为50% DECLARE_BITS_FIELD(POLAR, 0x0004, 2); // [2]时钟输出极性位 DECLARE_BITS_FIELD(DATOUT, 0x0002, 1); // [1]GPIO模式下,控制引脚输出电平 } }
// ----假设在zq_conf.h文件里---- namespace zq { namespace timer { // TIM0寄存器 DECLARE_REGISTER(TIM0, 0x1000); // TCR0寄存器 DECLARE_REGISTER(TCR0, 0x1002); } /*……其他外设寄存器定义……*/ } // ----假设在zq_timer.h文件里---- namespace zq { /*相当于对timer里寄存器的扩展*/ namespace timer { // TCRO寄存器位域 namespace TCR0 { DECLARE_BITS_FIELD(IDLEEN, 0x8000, 15); // [15] 0:不能进入IDLE状态 1:可以进入IDLE状态,当PERIS为1时进入IDLE状态 DECLARE_BITS_FIELD(INTEXT, 0x4000, 14); // [14]标志位:时钟源从内部切换为外部标志 0:外部时钟源没有准备好 1:外部时钟源已经准备好 DECLARE_BITS_FIELD(ERRTIM, 0x2000, 13); // [13]检测错误标志 0: 没有发生错误 1: 发生了错误 DECLARE_BITS_FIELD(FUNC, 0x1800, 11); // [11]工作模式选择位 DECLARE_BITS_FIELD(TLB, 0x0600, 10); // [10]定时器装载位 DECLARE_BITS_FIELD(SOFT, 0x0020, 9); // [9]软件触发位 DECLARE_BITS_FIELD(FREE, 0x0100, 8); // [8]与仿真断点有关 DECLARE_BITS_FIELD(PWID, 0x00C0, 6); // [6]窄脉冲输出宽度 每当TIM归零时,输出指定宽度的窄脉冲 DECLARE_BITS_FIELD(ARB, 0x0010, 5); // [5]自动重装控制位 DECLARE_BITS_FIELD(TSS, 0x0010, 4); // [4]定时器停止状态位 0:启动定时器 1:停止定时器 DECLARE_BITS_FIELD(CP, 0x0008, 3); // [3]定时器输出时钟/脉冲模式选择 0:脉冲模式 1:时钟模式,占空比固定为50% DECLARE_BITS_FIELD(POLAR, 0x0004, 2); // [2]时钟输出极性位 DECLARE_BITS_FIELD(DATOUT, 0x0002, 1); // [1]GPIO模式下,控制引脚输出电平 } class Timer0 { // 寄存器 typedef mmio::RegAccess<TCR0::REG> TCR0_REG; public: // 设置IDLE是否使能 0:禁止进入IDLE状态 1:允许进入IDLE状态 template<uint16_t mode> static void set_idle_mode() { TCR0_REG::modify_bits(mode, TCR0::IDLEEN::MASK, TCR0::IDLEEN::SHIFT); } }; }
引入模板
利用命名空间和宏,可以玩得相当花。但是这样调用寄存器进行操作并不方便,不能像嵌套的命名空间那般通过::和代码补全可以很方便地知道调用哪些函数。因此,可以利用模板或者直接写静态函数,再通过宏来封装。
这是封装寄存器的,主要用于不包含位域的寄存器
#define DECLARE_REGISTER(REG_NAME, ADDRESS)\ namespace REG_NAME\ {\ enum {\ REG = ADDRESS\ };\ INLINE static void write(uint16_t value) {\ *reinterpret_cast<volatile ioport uint16_t *>(ADDRESS) = value;\ }\ INLINE static uint16_t read() {\ return *reinterpret_cast<volatile ioport uint16_t *>(ADDRESS);\ }\ }
创建一个模板类,把前面进行位操作的函数定义塞进去。倘若直接使用宏来封装就有些麻烦,需要写很多的"\"
template<uint16_t address,uint16_t mask,uint16_t shift> class BitsField { INLINE volatile ioport uint16_t *ptr() { return reinterpret_cast<volatile ioport uint16_t *>(address); } public: // 单位操作(操作的是掩码) INLINE void set_bit() { *ptr() |= mask; } INLINE void clear_bit() { *ptr() &= ~mask; } // 读取位 INLINE bool read_bit() { return (*ptr() & mask) != 0; } INLINE bool read_bit_not() { return (*ptr() & mask) == 0; } /** * * @param value 要修改的值 */ INLINE void modify_bits(const uint16_t value) { *ptr() = (*ptr() & ~mask) | ((value << shift) & mask); } };
使用宏来封装
#define DECLARE_BITS_FIELD(REG_NAME,BITS_NAME, MASK_VAL, SHIFT_VAL)\ namespace REG_NAME\ {\ typedef BitsField<REG_NAME::REG,MASK_VAL,SHIFT_VAL> BITS_NAME;\ }
此时,在前面声明寄存器时,倘若不知道其是否有位域,也可直接定义映射这个寄存器。如果有,那么直接在后面写上或者那么在其他文件里再扩展。
// ----假设在zq_conf.h文件里---- namespace zq { namespace timer { // TIM0寄存器 DECLARE_REGISTER(TIM0, 0x1000); // TCR0寄存器 DECLARE_REGISTER(TCR0, 0x1002); } /*……其他外设寄存器定义……*/ } // ----假设在zq_timer.h文件里---- namespace zq { /*相当于对timer里寄存器的扩展*/ namespace timer { // TCRO寄存器位域 DECLARE_BITS_FIELD(TCR0,IDLEEN, 0x8000, 15); // [15] 0:不能进入IDLE状态 1:可以进入IDLE状态,当PERIS为1时进入IDLE状态 DECLARE_BITS_FIELD(TCR0,INTEXT, 0x4000, 14); // [14]标志位:时钟源从内部切换为外部标志 0:外部时钟源没有准备好 1:外部时钟源已经准备好 DECLARE_BITS_FIELD(TCR0,ERRTIM, 0x2000, 13); // [13]检测错误标志 0: 没有发生错误 1: 发生了错误 DECLARE_BITS_FIELD(TCR0,FUNC, 0x1800, 11); // [11]工作模式选择位 DECLARE_BITS_FIELD(TCR0,TLB, 0x0600, 10); // [10]定时器装载位 DECLARE_BITS_FIELD(TCR0,SOFT, 0x0020, 9); // [9]软件触发位 DECLARE_BITS_FIELD(TCR0,FREE, 0x0100, 8); // [8]与仿真断点有关 DECLARE_BITS_FIELD(TCR0,PWID, 0x00C0, 6); // [6]窄脉冲输出宽度 每当TIM归零时,输出指定宽度的窄脉冲 DECLARE_BITS_FIELD(TCR0,ARB, 0x0010, 5); // [5]自动重装控制位 DECLARE_BITS_FIELD(TCR0,TSS, 0x0010, 4); // [4]定时器停止状态位 0:启动定时器 1:停止定时器 DECLARE_BITS_FIELD(TCR0,CP, 0x0008, 3); // [3]定时器输出时钟/脉冲模式选择 0:脉冲模式 1:时钟模式,占空比固定为50% DECLARE_BITS_FIELD(TCR0,POLAR, 0x0004, 2); // [2]时钟输出极性位 DECLARE_BITS_FIELD(TCR0,DATOUT, 0x0002, 1); // [1]GPIO模式下,控制引脚输出电平 } }
调用时就可以更加随心所欲了,如此一来,映射寄存器方便,使用也方便,而且都是编译器计算并不没有额外开销。
可以清晰地看出,红色的是直接对寄存器进行操作的函数,绿色是枚举体里记录的寄存器地址,紫色是寄存器里的位域(如果这个寄存器有位域的话,没有就不会弹出这些提示)。
位域里会自动弹出相关位操作,可以看到其中的ptr旁边的锁是红色的,因为这个是私有函数。本来以为不会暴露出来的,不过也很好解决,把所有*ptr()全部替换为强制转换即可
不过,那样会很长,该使用宏就使用宏
#define MEM_MAP(Address) reinterpret_cast<volatile ioport uint16_t *>(Address)
如此一来便有
template<uint16_t address,uint16_t mask,uint16_t shift> struct BitsField { // 单位操作(操作的是掩码) INLINE void set_bit() { *MEM_MAP(address) |= mask; } INLINE void clear_bit() { *MEM_MAP(address) &= ~mask; } // 读取位 INLINE bool read_bit() { return (*MEM_MAP(address) & mask) != 0; } INLINE bool read_bit_not() { return (*MEM_MAP(address) & mask) == 0; } /** * * @param value 要修改的值 */ INLINE void modify_bits(const uint16_t value) { *MEM_MAP(address) = (*MEM_MAP(address) & ~mask) | ((value << shift) & mask); } };
模板特化
考虑到位域中的有些操作位是单个位,而有些是多位,前者只需要一个掩码即可,而后者则需要位移,因此可以使用模板偏特化。
// 把地址映射为存储空间指针,也就是片内寄存器 #define MEM_MAP(Address) reinterpret_cast<volatile ioport uint16_t *>(Address) // 根据位宽获取掩码 #define GET_BITS_MASK(WIDTH,SHIFT) (((1<<(WIDTH))-1)<<(SHIFT)) // 位域模板结构体 template<uint16_t address,uint16_t mask,uint16_t shift> struct BitsField { // 设置值 INLINE void write_bits(const uint16_t value) { *MEM_MAP(address) = (*MEM_MAP(address) & ~mask) | ((value << shift) & mask); } // 读取值 INLINE uint16_t read_bits() { return (*MEM_MAP(address) & mask) >> shift; } // 清除多位 INLINE void clear_bits() { *MEM_MAP(address) &= ~mask; } }; // 位域模板结构体 template<uint16_t address,uint16_t shift> struct BitsField<address,0,shift> { // 单位操作(操作的是掩码) INLINE void set_bit() { *MEM_MAP(address) |= (1<<shift); } INLINE void clear_bit() { *MEM_MAP(address) &= ~(1<<shift); } // 修改位 INLINE void write_bit(const bool value) { *MEM_MAP(address) = (*MEM_MAP(address) & ~(1<<shift)) | ((value << shift) & (1<<shift)); } // 读取位 INLINE bool read_bit() { return (*MEM_MAP(address) & (1<<shift)) != 0; } INLINE bool read_bit_not() { return (*MEM_MAP(address) & (1<<shift)) == 0; } };
宏也改一下,单位操作和多位操作虽然宏名称很像,易出错,可自行更改名称。同时,用位宽转换代替掩码,让声明更加方便(前面bit4那个没发现的掩码错误也分辨出来了哈哈)
// 寄存器声明 #define DECLARE_REGISTER(REG_NAME, ADDRESS)\ namespace REG_NAME\ {\ enum {\ REG = ADDRESS\ };\ INLINE static void write(uint16_t value) {\ *MEM_MAP(ADDRESS) = value;\ }\ INLINE static uint16_t read() {\ return *MEM_MAP(ADDRESS);\ }\ } // 单位字段声明 #define DECLARE_BIT_FIELD(REG_NAME,BITS_NAME, SHIFT)\ namespace REG_NAME\ {\ typedef BitsField<REG_NAME::REG,0,SHIFT> BITS_NAME;\ } // 多位字段声明 #define DECLARE_BITS_FIELD(REG_NAME,BITS_NAME, WIDTH, SHIFT)\ namespace REG_NAME\ {\ typedef BitsField<REG_NAME::REG,GET_BITS_MASK(WIDTH,SHIFT),SHIFT> BITS_NAME;\ }
如此声明,便不容易错
/*相当于对timer里寄存器的扩展*/ namespace timer { // TCRO寄存器位域 DECLARE_BIT_FIELD(TCR0,IDLEEN,15); // [15] 0:不能进入IDLE状态 1:可以进入IDLE状态,当PERIS为1时进入IDLE状态 DECLARE_BIT_FIELD(TCR0,INTEXT,14); // [14]标志位:时钟源从内部切换为外部标志 0:外部时钟源没有准备好 1:外部时钟源已经准备好 DECLARE_BIT_FIELD(TCR0,ERRTIM,13); // [13]检测错误标志 0: 没有发生错误 1: 发生了错误 DECLARE_BIT_FIELD(TCR0,FUNC,11); // [11]工作模式选择位 DECLARE_BIT_FIELD(TCR0,TLB, 10); // [10]定时器装载位 DECLARE_BIT_FIELD(TCR0,SOFT,9); // [9]软件触发位 DECLARE_BIT_FIELD(TCR0,FREE,8); // [8]与仿真断点有关 DECLARE_BIT_FIELD(TCR0,PWID,6); // [6]窄脉冲输出宽度 每当TIM归零时,输出指定宽度的窄脉冲 DECLARE_BIT_FIELD(TCR0,ARB,5); // [5]自动重装控制位 DECLARE_BIT_FIELD(TCR0,TSS,4); // [4]定时器停止状态位 0:启动定时器 1:停止定时器 DECLARE_BIT_FIELD(TCR0,CP,3); // [3]定时器输出时钟/脉冲模式选择 0:脉冲模式 1:时钟模式,占空比固定为50% DECLARE_BIT_FIELD(TCR0,POLAR,2); // [2]时钟输出极性位 DECLARE_BIT_FIELD(TCR0,DATOUT,1); // [1]GPIO模式下,控制引脚输出电平 }
模板类继承(另一套)
考虑到一些特殊情况,比如定时器的寄存器,TIM0和TIM1、PRD0和PRD1等的位域相同,只是寄存器地址的偏移不同。如果若使用前面基于命名空间的办法,那么只得定义两个寄存器,代码翻倍。
如果想要只使用单个寄存器,那么可以使用模板,而命名空间是无法放在结构体或者类里的。因此可以使用模板类的继承和一些宏的奇技淫巧。
为了与前面的接口保持一致,需要定义两个基类,一个用作寄存器,一个用作位域。先定义寄存器的模板类,用作基类
// 寄存器声明 template<uint32_t address> struct RegisterAccess { // 单位操作(操作的是掩码) INLINE void write(const uint16_t shift) { *MEM_MAP(address) |=(1<<shift); } INLINE void write(const uint16_t shift,const bool data) { *MEM_MAP(address) =(*MEM_MAP(address) & ~(1<<shift)) |((data << shift) & (1<<shift)); } INLINE uint16_t read() { return *MEM_MAP(address); } INLINE bool read(const bool shift) { return ((*MEM_MAP(address)) & (1<<shift)) != 0; } INLINE void clear() { *MEM_MAP(address) = 0; } // 单位操作(操作的是掩码) INLINE void clear(const bool shift) { *MEM_MAP(address) &= ~(1<<shift); } };
前面的BitsField也用作基类。那么这个新的寄存器应为如下形式,API调用与前面保持一致
// TCR寄存器 struct TCR:RegisterAccess<0x1002> { // IDLEEN位 bit15 struct IDLEEN:BitsField<0x1002,0,15>{}; // INTEXT位 bit14 struct INTEXT:BitsField<0x1002,0,14>{}; };
模仿Verilog的begin、end语法,可以定义这样的宏。末尾加个T,表示这个宏的上面可以使用模板
// ==========寄存器声明(可使用模板)========== #define DECLARE_REGISTER_T(REG_NAME,ADDRESS)\ struct REG_NAME:RegisterAccess<ADDRESS>\ {}; // ========寄存器开始======== #define BEGIN_REG_T(REG_NAME, ADDRESS)\ struct REG_NAME:RegisterAccess<ADDRESS>\ {\ enum\ {\ REG = ADDRESS\ }; // ===单位字段声明,必须放在前后的宏之间=== #define DECLARE_BIT_FIELD_T(BIT_NAME, SHIFT)\ struct BIT_NAME: BitsField<REG,0,SHIFT>\ {}; // ===多位字段声明,必须放在前后的宏之间=== #define DECLARE_BITS_FIELD_T(BITS_NAME, WIDTH, SHIFT)\ struct BITS_NAME: BitsField<REG,GET_BITS_MASK(WIDTH,SHIFT),SHIFT>\ {}; // ========寄存器结束======== #define END_REG_T()\ };
那么现在我们可以这般使用。如果觉得END_REG_T()后面的括号不好看(类cmake),也可去除。
// ========定时器寄存器======== template<uint16_t offset> DECLARE_REGISTER_T(TIM, 0x1000+offset); // 主计数器 template<uint16_t offset> DECLARE_REGISTER_T(PRD, 0x1001+offset); // 预装载计时器 // TCR寄存器位域 template<uint16_t offset> BEGIN_REG_T(TCR, 0x1002+offset); // 控制寄存器 DECLARE_BIT_FIELD_T(IDLEEN, 15); // [15] 0:不能进入IDLE状态 1:可以进入IDLE状态,当PERIS为1时进入IDLE状态 DECLARE_BIT_FIELD_T(INTEXT, 14); // [14]标志位:时钟源从内部切换为外部标志 0:外部时钟源没有准备好 1:外部时钟源已经准备好 DECLARE_BIT_FIELD_T(ERRTIM, 13); // [13]检测错误标志 0: 没有发生错误 1: 发生了错误 DECLARE_BIT_FIELD_T(FUNC, 11); // [11]工作模式选择位 DECLARE_BIT_FIELD_T(TLB, 10); // [10]定时器装载位 DECLARE_BIT_FIELD_T(SOFT, 9); // [9]软件触发位 DECLARE_BIT_FIELD_T(FREE, 8); // [8]与仿真断点有关 DECLARE_BIT_FIELD_T(PWID, 6); // [6]窄脉冲输出宽度 每当TIM归零时,输出指定宽度的窄脉冲 DECLARE_BIT_FIELD_T(ARB, 5); // [5]自动重装控制位 DECLARE_BIT_FIELD_T(TSS, 4); // [4]定时器停止状态位 0:启动定时器 1:停止定时器 DECLARE_BIT_FIELD_T(CP, 3); // [3]定时器输出时钟/脉冲模式选择 0:脉冲模式 1:时钟模式,占空比固定为50% DECLARE_BIT_FIELD_T(POLAR, 2); // [2]时钟输出极性位 DECLARE_BIT_FIELD_T(DATOUT, 1); // [1]GPIO模式下,控制引脚输出电平 END_REG_T() // PRSC寄存器位域 template<uint16_t offset> BEGIN_REG_T(PRSC, 0x1003+offset);// 分频寄存器 DECLARE_BITS_FIELD_T(PSC, 4, 6); // 预分频值bit6~9 DECLARE_BITS_FIELD_T(TDDR, 3, 0); // 用于装入PSC中 END_REG_T() /** * @brief 通用定时器模板类 * @tparam offset 定时器偏移量 * @detail 提供统一的定时器操作接口,通过模板参数区分不同定时器实例 */ template<Offset::Type offset> class Timer { // 寄存器映射 typedef TIM<offset> TIM_REG; typedef PRD<offset> PRD_REG; typedef PRSC<offset> PRSC_REG; typedef TCR<offset> TCR_REG; public: /*…………*/ };
寄存器映射框架代码
宏的一些模仿罢
/** *快速定义属性类型 * @example * DECLARE_ATTRIBUTE(Mode, * HIZ = 0b00, // TIN/TOUT为高阻态,时钟源是内部CPU时钟源 * OUTPUT = 0b01, // TIN/TOUT为定时器输出,时钟源是内部CPU时钟 * GPIO = 0b10, // TIN/TOUT为通用输出,引脚电平反映的是DATOUT位的值 * EXT_INPUT = 0b11 // TIN/TOUT为定时器输入,时钟源是外部时钟 * ); * 使用方法既可以定义这个类型(模拟enum class),也可以直接通过NAME来访问。比如 * void func(NAME::Type mode); * NAME::Type mode = NAME::Type::HIZ; **/ #define DECLARE_ATTRIBUTE(NAME, ...) \ struct NAME { \ typedef enum { \ __VA_ARGS__ \ } Type; \ } // ========================================寄存器映射(命名空间法)=========================================== // 把地址映射为存储空间指针,也就是片内寄存器 #define MEM_MAP(Address) reinterpret_cast<volatile ioport uint16_t *>(Address) #define EXMEM_MAP(Address) reinterpret_cast<uint32_t *>(Address) // 根据位宽获取掩码 #define GET_BITS_MASK(WIDTH,SHIFT) (((1<<(WIDTH))-1)<<(SHIFT)) // 位域模板结构体 template<uint16_t address,uint16_t mask,uint16_t shift> struct BitsField { // 设置值 INLINE void write_bits(const uint16_t value) { *MEM_MAP(address) = (*MEM_MAP(address) & ~mask) | ((value << shift) & mask); } // 读取值 INLINE uint16_t read_bits() { return (*MEM_MAP(address) & mask) >> shift; } // 清除多位 INLINE void clear_bits() { *MEM_MAP(address) &= ~mask; } }; // 位域模板结构体 template<uint16_t address,uint16_t shift> struct BitsField<address,0,shift> { // 单位操作(操作的是掩码) INLINE void set_bit() { *MEM_MAP(address) |= (1<<shift); } INLINE void clear_bit() { *MEM_MAP(address) &= ~(1<<shift); } // 修改位 INLINE void write_bit(const bool value) { *MEM_MAP(address) = (*MEM_MAP(address) & ~(1<<shift)) | ((value << shift) & (1<<shift)); } // 读取位 INLINE bool read_bit() { return (*MEM_MAP(address) & (1<<shift)) != 0; } INLINE bool read_bit_not() { return (*MEM_MAP(address) & (1<<shift)) == 0; } }; // 寄存器声明 #define DECLARE_REGISTER(REG_NAME, ADDRESS)\ namespace REG_NAME\ {\ enum {\ REG = ADDRESS\ };\ INLINE void write(uint16_t value) {\ *MEM_MAP(ADDRESS) = value;\ }\ INLINE uint16_t read() {\ return *MEM_MAP(ADDRESS);\ }\ } // 单位字段声明 #define DECLARE_BIT_FIELD(REG_NAME,BITS_NAME, SHIFT)\ namespace REG_NAME\ {\ typedef BitsField<REG_NAME::REG,0,SHIFT> BITS_NAME;\ } // 多位字段声明 #define DECLARE_BITS_FIELD(REG_NAME,BITS_NAME, WIDTH, SHIFT)\ namespace REG_NAME\ {\ typedef BitsField<REG_NAME::REG,GET_BITS_MASK(WIDTH,SHIFT),SHIFT> BITS_NAME;\ } // ========================================寄存器映射(模板类法)=========================================== // 寄存器声明 template<uint16_t address> struct RegisterAccess { // 单位操作(操作的是掩码) INLINE void write(const uint16_t shift) { *MEM_MAP(address) |=(1<<shift); } INLINE void write(const uint16_t shift,const bool data) { *MEM_MAP(address) =(*MEM_MAP(address) & ~(1<<shift)) |((data << shift) & (1<<shift)); } INLINE uint16_t read() { return *MEM_MAP(address); } INLINE bool read(const uint16_t shift) { return ((*MEM_MAP(address)) & (1<<shift)) != 0; } INLINE void clear() { *MEM_MAP(address) = 0; } // 单位操作(操作的是掩码) INLINE void clear(const bool shift) { *MEM_MAP(address) &= ~(1<<shift); } }; // ========寄存器声明======== #define DECLARE_REGISTER_T(REG_NAME,ADDRESS)\ struct REG_NAME:RegisterAccess<ADDRESS>\ {}; #define BEGIN_REG_T(REG_NAME, ADDRESS)\ struct REG_NAME:RegisterAccess<ADDRESS>\ {\ enum\ {\ REG = ADDRESS\ }; // 单位字段声明,必须放在前后的宏之间 #define DECLARE_BIT_FIELD_T(BIT_NAME, SHIFT)\ struct BIT_NAME: BitsField<REG,0,SHIFT>\ {}; // 多位字段声明,必须放在前后的宏之间 #define DECLARE_BITS_FIELD_T(BITS_NAME, WIDTH, SHIFT)\ struct BITS_NAME: BitsField<REG,GET_BITS_MASK(WIDTH,SHIFT),SHIFT>\ {}; // ========寄存器结束======== #define END_REG_T()\ }; // ===============================外部存储器=================================== // 32位的地址 template<uint32_t address> struct ExMemAccess { // 单位操作,把该位变为1 INLINE void write(const uint16_t value) { *EXMEM_MAP(address) =value; } INLINE void write(const uint16_t shift,const bool data) { *EXMEM_MAP(address) =(*EXMEM_MAP(address) & ~(1<<shift)) |((data << shift) & (1<<shift)); } INLINE uint16_t read() { return *EXMEM_MAP(address); } // 返回的是单位的值 INLINE bool read(const uint16_t shift) { return ((*EXMEM_MAP(address)) & (1<<shift)) != 0; } INLINE void clear() { *EXMEM_MAP(address) = 0; } // 单位操作(操作的是掩码) INLINE void clear(const bool shift) { *EXMEM_MAP(address) &= ~(1<<shift); } }; // 外部存储器寄存器声明 #define DECLARE_EXMEM_REGISTER_T(REG_NAME,ADDRESS)\ struct REG_NAME:ExMemAccess<ADDRESS> {};
3,CPU寄存器
先来一个CPU寄存器练练手,根据手册我们可以看到有大量的寄存器需要定义,一个一个手写自然是不可能的,可以先写几行代码再让DS根据手册来补全
多次对话并整理,最后得到这样的代码
// // Created by fairy on 2025/3/30 18:25. // #ifndef ZQ_CPU_H #define ZQ_CPU_H #include<zq_conf.h> namespace zq { namespace cpu { // C55x的一堆CPU寄存器映射 // 中断控制类寄存器 DECLARE_REGISTER(IER0,0x0000); // 中断使能寄存器0 [15-0] DECLARE_REGISTER(IFR0,0x0001); // 中断标志寄存器0 [15-0] // 状态寄存器组 DECLARE_REGISTER(ST0_55,0x0002); // C55x状态寄存器0 [15-0] DECLARE_REGISTER(ST1_55,0x0003); // C55x状态寄存器1 [15-0] DECLARE_REGISTER(ST3_55,0x0004); // C55x状态寄存器3 [15-0] DECLARE_REGISTER(RSVD05,0x0005); // 保留区域 [15-0] DECLARE_REGISTER(ST0,0x0006); // 标准状态寄存器0 [15-0] DECLARE_REGISTER(ST1,0x0007); // 标准状态寄存器1 [15-0] // 累加器寄存器组 DECLARE_REGISTER(AC0L,0x0008); // 低16位数据 [15-0] DECLARE_REGISTER(AC0H,0x0009); // 高16位数据 [31-16] DECLARE_REGISTER(AC0G,0x000A); // 保护位 [39-32] DECLARE_REGISTER(AC1L,0x000B); // 低16位数据 [15-0] DECLARE_REGISTER(AC1H,0x000C); // 高16位数据 [31-16] DECLARE_REGISTER(AC1G,0x000D); // 保护位 [39-32] // 运算辅助寄存器 DECLARE_REGISTER(TREG,0x000E); // 临时数据寄存器 [15-0](原T3) DECLARE_REGISTER(TRN0,0x000F); // 过渡寄存器 [15-0] // 地址指针寄存器 DECLARE_REGISTER(AR0,0x0010); // 辅助寄存器0 [15-0] DECLARE_REGISTER(AR1,0x0011); // 辅助寄存器1 [15-0] DECLARE_REGISTER(AR2,0x0012); // 辅助寄存器2 [15-0] DECLARE_REGISTER(AR3,0x0013); // 辅助寄存器3 [15-0] DECLARE_REGISTER(AR4,0x0014); // 辅助寄存器4 [15-0] DECLARE_REGISTER(AR5,0x0015); // 辅助寄存器5 [15-0] DECLARE_REGISTER(AR6,0x0016); // 辅助寄存器6 [15-0] DECLARE_REGISTER(AR7,0x0017); // 辅助寄存器7 [15-0] // 系统控制类寄存器 DECLARE_REGISTER(SP, 0x0018); // 栈指针寄存器 [15-0] DECLARE_REGISTER(BK03,0x0019); // 循环缓冲区大小寄存器 [15-0] DECLARE_REGISTER(BRC0,0x001A); // 块重复计数器 [15-0] DECLARE_REGISTER(RSAOL,0x001B); // 块重复起始地址寄存器 [15-0] DECLARE_REGISTER(REAOL,0x001C); // 块重复结束地址寄存器 [15-0] DECLARE_REGISTER(PMST,0x001D); // 处理器模式状态寄存器 [15-0] DECLARE_REGISTER(XPC, 0x001E); // 程序计数器扩展寄存器 [7-0] DECLARE_REGISTER(RSVD1F,0x001F); // 保留寄存器 [15-0] // 数据操作类寄存器 DECLARE_REGISTER(T0, 0x0020); // 临时数据寄存器0 [15-0] DECLARE_REGISTER(T1, 0x0021); // 临时数据寄存器1 [15-0] DECLARE_REGISTER(T2, 0x0022); // 临时数据寄存器2 [15-0] DECLARE_REGISTER(T3, 0x0023); // 临时数据寄存器3 [15-0] // 累加器扩展(AC2) DECLARE_REGISTER(AC2L,0x0024); // 累加器2低16位 [15-0] DECLARE_REGISTER(AC2H,0x0025); // 累加器2高16位 [31-16] DECLARE_REGISTER(AC2G,0x0026); // 累加器2保护位 [39-32] // 数据指针类寄存器 DECLARE_REGISTER(CDP, 0x0027); // 系数数据指针 [15-0] // 累加器扩展(AC3) DECLARE_REGISTER(AC3L, 0x0028); // 累加器3低16位 [15-0] DECLARE_REGISTER(AC3H, 0x0029); // 累加器3高16位 [31-16] DECLARE_REGISTER(AC3G, 0x002A); // 累加器3保护位 [39-32] // 内存管理单元 DECLARE_REGISTER(DPH, 0x002B); // 扩展数据页指针 [6-0] DECLARE_REGISTER(MDP05,0x002C); // 保留寄存器 [6-0] DECLARE_REGISTER(MDP67,0x002D); // 保留寄存器 [15-0] DECLARE_REGISTER(DP, 0x002E); // 二级数据页指针 [15-0] DECLARE_REGISTER(PDP, 0x002F); // 外设数据页起始地址 [8-0] // 循环缓冲控制类 DECLARE_REGISTER(BK47, 0x0030); // AR4-7循环缓冲区大小 [15-0] DECLARE_REGISTER(BKC, 0x0031); // CDP循环缓冲区大小 [15-0] DECLARE_REGISTER(BSA01,0x0032); // AR0-1循环起始地址 [15-0] DECLARE_REGISTER(BSA23,0x0033); // AR2-3循环起始地址 [15-0] DECLARE_REGISTER(BSA45,0x0034); // AR4-5循环起始地址 [15-0] DECLARE_REGISTER(BSA67,0x0035); // AR6-7循环起始地址 [15-0] DECLARE_REGISTER(BSAC, 0x0036); // 系数循环起始地址 [15-0] DECLARE_REGISTER(BIOS, 0x0037); // 数据页指针存储(128字数据表)[15-0] // 特殊功能寄存器 DECLARE_REGISTER(TRN1, 0x0038); // 过渡寄存器1 [15-0](原BIOS) DECLARE_REGISTER(BRC1,0x0039); // 块重复计数器1 [15-0] DECLARE_REGISTER(BRS1,0x003A); // 块重复保存寄存器1 [15-0] DECLARE_REGISTER(CSR, 0x003B); // 单次重复计算寄存器 [15-0] DECLARE_REGISTER(RSA0H,0x003C); // 块重复起始地址高位 [23-16] // 重复地址类寄存器 DECLARE_REGISTER(RSA0L, 0x003D); // 重复起始地址0低16位 [15-0] DECLARE_REGISTER(REA0H, 0x003E); // 重复起始地址0高8位 [23-16] DECLARE_REGISTER(REA0L, 0x003F); // 重复结束地址0低16位 [15-0] DECLARE_REGISTER(RSA1H, 0x0040); // 重复起始地址1高8位 [23-16] DECLARE_REGISTER(RSA1L, 0x0041); // 重复起始地址1低16位 [15-0] DECLARE_REGISTER(REA1H, 0x0042); // 重复结束地址1高8位 [23-16] DECLARE_REGISTER(REA1L, 0x0043); // 重复结束地址1低16位 [15-0] // 控制计数器类 DECLARE_REGISTER(RPTC, 0x0044); // 重复计数器 [15-0] // 中断调试类 DECLARE_REGISTER(IER1, 0x0045); // 中断使能寄存器1 [15-0] DECLARE_REGISTER(IFR1, 0x0046); // 中断标志寄存器1 [15-0] DECLARE_REGISTER(DBIER0, 0x0047); // 调试中断使能寄存器0 [15-0] DECLARE_REGISTER(DBIER1, 0x0048); // 调试中断使能寄存器1 [15-0] // 系统类寄存器 DECLARE_REGISTER(IVPD, 0x0049); // 中断向量页指针 [15-0] DECLARE_REGISTER(IVPH, 0x004A); // 中断向量页指针 [15-0] DECLARE_REGISTER(ST2_55, 0x004B); // 保留区域 [15-0] DECLARE_REGISTER(SSP, 0x004C); // 系统堆栈指针 [15-0] DECLARE_REGISTER(USP, 0x004D); // 用户堆栈指针 [15-0](原为SP) DECLARE_REGISTER(SPH, 0x004E); // 扩展堆栈页指针 [6-0] DECLARE_REGISTER(CDPH, 0x004F); // CDP高位页指针 [6-0] } } #endif //ZQ_CPU_H
……
八、小型GUI系统
先开坑,不确定后面有没有时间填上
1,简介
这种DSP芯片资源较为紧张,一般用单色屏,此处就以128*64的OLED屏为例。
2,搭建模拟器
同简易LVGL模拟器,使用SDL2构建。
3,设计思路
设计思路模仿的是LVGL,不过采用的是固定大小的脏矩形绘制,不包含脏矩形合并操作。根据该OLED屏的特性,共分为8页。
上面思路有些麻烦,不利于短时间开发
【已填坑】
九、使用CLion配置原生工程
理论上来说,应该使用CMake构建系统,毕竟这个又快又好用。但是CLion并不支持TI的调试,也就是说TI项目的调试必须在CCS中,因此CLion在此处只用作编译器和文件编辑器使用,使用CCS的Makefile构建系统就不需要考虑两者同步的问题。
建议有使用过CLion或者VS Code开发嵌入式的开发者再观看下面内容
1,自定义构建
先使用CLion打开前面的工程demo
自定义构建目标
添加一个自定义构建目标,工具链随便选择一个,这里不用它。
根据控制台输出,我们可以添加一个构建目标
实参就是gmake后面的参数“-k -j 16 all -O”,由于makefile是在Debug目录的,所以这里把工作目录选择到工程目录下的Debug目录。gmake这个程序根据上面的控制台输出提示就能看到。
选择刚才创建的构建目标
添加配置
回到主界面,点击编辑配置
选择自定义构建应用程序
目标就是前面自定义的构建目标,名称随意起一个
构建
点击小锤子,可以看到下面的控制台输出信息与CCS别无二致
添加清理目标
前面已经添加了自定义构建目标,现在可以根据CCS清理时的输出提示可知,这个目标与前面的区别只是把all换成了clean
工作目录也需要设置为Debug
不过配置后暂时还有用到的地方
注意事项
前面可以看到,配置这个构建目标是依赖于Debug下的Makefile,而这个Makefile是由CCS自动生成的。也就是说如果在项目里添加了新文件,需要先在CCS那里添加对应的头文件目录等,确保在CCS那里能通过编译。
2,Makefile构建目标(推荐)
原TI工程就是建立在Makefile构建体系上(gmake),那么可以定义一个Makefile目标,这样可以更轻松地使用目标。
配置目标
与前面操作差不多,这里添加一个Makefile目标,然后把Makefile、目标、实参和工作目录都填写上。
不过要注意,由于Debug目录下的Makefile文件是“makefile”,所以这里选择Makefile路径时,其实是无法识别的。只需要随便选择一个路径,然后再在输入框里修改即可
下面是填写后all目标后的
填写clean目标与之类似
可以这般快速切换目标
指定make工具
填写目标后,这个make还是不能使用的,因为没有指定make工具。在设置里找到下面这个选项,然后把CCS里的gmake路径填上去即可
使用Makefile目标对于make构建体系是非常方便的,且很容易修改一些标志或者环境变量,而自定义构建目标主要是更加自由,可以适应更多情况。
十、使用CLion配置CMake工程
1,C5xx CGT
使用TI的编译工具集来编译文件。如前面所示,配置了Makefile目标。因为工程伊始并没有CMakeLists,所以编写CMakeLists之后,CLion不会将项目作为cmake项目。
根据makefile和CCS编译器链接器配置里面的内容,我们可以编写一个简单的CMakeLists
set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_VERSION 1) cmake_minimum_required(VERSION 3.29) # 指定CGT #set(CMAKE_C_COMPILER_WORKS 1) # 强制绕过编译器检查 set(CMAKE_C_COMPILER [[E:\Tools\Develop\ToolsKits\TI\C5500_Code_Generation_Tools4.4.1\bin\cl55.exe]]) # TI C55x编译器路径 set(CMAKE_CXX_COMPILER ${CMAKE_C_COMPILER}) set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER}) # 强制禁用响应文件 set(CMAKE_C_USE_RESPONSE_FILE_FOR_OBJECTS OFF) set(CMAKE_CXX_USE_RESPONSE_FILE_FOR_OBJECTS OFF) # 项目名称 project(TI C CXX ASM) # =====================设置项目文件===================== set(MAP_FILE "${CMAKE_SOURCE_DIR}/Debug/demo.map") set(LINKINFO_XML "${CMAKE_SOURCE_DIR}/Debug/demo_linkInfo.xml") # ===================设置工程目录====================== set(APP_DIR App/demo) # ===================设置工程目录====================== # =======添加编译标志======= set(CMAKE_C_FLAGS "-v5509A") #add_compile_options("-v5509A") add_compile_definitions(CHIP_5509A;c5509a) add_compile_options(--memory_model=small --ptrdiff_size=16) add_compile_options(--relaxed_ansi --gcc) add_compile_options(--display_error_number --diag_warning=225) # 编译优化 add_compile_options(-O -g) # =======添加链接标志======= # 设置链接脚本 #add_link_options("-v5509A") #add_link_options(--memory_model=small -g) #add_link_options(--relaxed_ansi --gcc) add_link_options(--define=c5509a --define=CHIP_5509A) add_link_options(--display_error_number --diag_warning=225 -z -m${MAP_FILE} --stack_size=0x200 --heap_size=0x400) add_link_options(--reread_libs --display_error_number --warn_sections) add_link_options(--xml_link_info=${LINKINFO_XML} --rom_model --sys_stacksize=0x200) ## 调试与预处理配置[3](@ref) #set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # 生成编译数据库 #set(CMAKE_VERBOSE_MAKEFILE ON) # 显示详细编译命令 #-------------------------------------------库------------------------------------------- file(GLOB_RECURSE SOURCES "${APP_DIR}/*.c" "BSP/src/*.c" "Core/src/*.c" ) include_directories([[E:/Tools/Develop/ToolsKits/TI/C5500_Code_Generation_Tools4.4.1/include]] "App" "App/conf" "BSP/inc" "Core/Drivers/inc" ) set(TARGET_FILE ${PROJECT_NAME}.out) add_executable(${TARGET_FILE} ${SOURCES}) # 链接库 target_include_directories(${TARGET_FILE} PRIVATE "E:/Tools/Develop/ToolsKits/TI/C5500_Code_Generation_Tools4.4.1/include" ) target_link_directories(${TARGET_FILE} PRIVATE "E:/Tools/Develop/ToolsKits/TI/C5500_Code_Generation_Tools4.4.1/lib" ) # 添加自定义命令: # 生成十六进制和二进制文件 add_custom_command(TARGET ${TARGET_FILE} POST_BUILD COMMENT "Building " )
最直接的解决方式,就是关闭CLion并打开项目目录,把CLion在项目目录的配置信息.idea目录给删除
让其重新加载,就能识别为cmake项目了。这里先随便选择一个工具链并点击确定,或者点击x关闭这个窗口
配置工具链
CLion默认支持MinGW、Cygwin、VC等工具链,但C55x CGT属于TI自研的工具链,所以CLion不直接支持。那么我们可以添加一个系统工具链
指定好构建工具gmake,C/C++编译器都选cl55
配置自定义编译器文件
为了能让cmake正常运行,不会报无法识别编译器的错误,我们需要为这位陌生的编译器先生配置一个yaml文件
那么先随便写一个简单的yaml文件
compilers: - description: TI C5500 Compiler v4.4.1 match-compiler-exe: "(.*/)?cl55(.exe)?" match-language: C code-insight-target-name: msp430 # Clangd 的目标平台名称 include-dirs: ${compiler-exe-dir}/../include # 特定于编译器的标头搜索路径 # 宏定义配置(对象结构) defines: template: "--define=<NAME>$if(<VALUE>):=<VALUE>" separator: " " # 多个宏之间的分隔符 # 包含目录配置 match-args: []
在CLion的这个选项里使用自定义配置
修改配置
在配置这里选择前面配置好的工具链,生成器使用默认值
构建
然后我们刷新cmake后,点击小锤子开始构建。需要等待一会,最后就能生成指定的.out文件。这个花花绿绿的输出提示是make构建工具的祖传显示
构建工具的选择
make系列的工具终归有些慢,我们换成nija
使用Nija构建,没有make系列那样花花绿绿,虽然观赏效果损失了一大半,但Nija构建速度远超gmake。以此处工程为例(只有两个源文件),gmake耗时10秒左右,而Nija只要一瞬,1s左右。
如果既想要gmake那样花花绿绿的输出提示,又想要不慢的构建速度,那么可以选用make系列里的MinGW Makefile
比gmake快得多,大概两秒,稍逊于Nija,同时又保留了make系列的花花绿绿
与CCS的同步
为了确保在CCS里不会误把cmake的构建目录当成资源目录,从而引发函数重定义问题,那么需要把CLion的构建目录设在Debug目录下,并把之前的cmake构建目录删除
静态检查
有些关键字是TI编译器特有的,Clangd是无法识别的,比如ioport。为了让静态检查不报错,可以在CMakeLists添加该宏。但是该操作会让TI编译器无法正常编译,因为在传入给cl55的参数中,会有一个ioport的宏,导致cl55把关键字ioport先进行预处理为了空格。
更好的办法是,修改.yaml或者.clangd。这里以.clangd为例,在项目目录下(与CMakeLists同级)创建一个.clangd文件,可在里面添加ioport进行宏替换,把它当成编译器扩展
CompileFlags: Add: - Dioprot=__attribute__((ioport)) Compiler: cl55
或者让clangd彻底忽略
ioport
或将其识别为合法关键字CompileFlags: Add: - -Wno-unknown-attributes
2,GCC【推荐】
原因
前面那个自定义工具链已经实现了,可以让CLion编译TI的可执行文件。但有一个相当头疼的事情,那就是CLion的代码补全、代码提示、代码格式化什么都没有了,只有静态检查偶尔还能运行。
使用CCS没有代码补全等功能,使用CLion功能也没有代码补全功能,那么CLion不就白用了吗
为了能让我们想要的功能回来,那么只能给其配置一个CLion支持的工程,比如GCC工程。那么只能把这个DSP工程当成一个普通的单片机工程,然后用GCC编译它。
这样做缺点也很明显,比如有些cl55上专有的语法,GCC是无法通过编译的,只能通过添加额外的条件预编译和宏,让其只对TI编译器开放。好在cl55的绝大部分语法与C/C++标准别无二致,那么代码检查这一块就可以放宽心了。
不过既然在这里使用GCC工程了,那么只需要CLion当成一个好用的代码编辑器。我们需要为其编写特定的CMakeLists文件和链接脚本来尽可能模仿cl55所处的环境。
这里使用的是arm-gcc,也就是交叉编译为arm架构的,之所以不是直接使用MinGW32/64,是因为这玩意太过庞大了,会强制带上一些不必要的东西,很难模仿cl55。
cmake配置
同时为了能让工程可以快速从TI编译工具链和GCC编译工具链来回切换,既不影响编译,又不影响代码编辑,那么可以创建两个子cmake。一个作为cl55下的编译环境,另一个作为gcc下的编译环境。通过include来选择性包含
ti_c5500.cmake
# 指定CGT #set(CMAKE_C_COMPILER_WORKS 1) # 强制绕过编译器检查 # 强制禁用响应文件 #set(CMAKE_C_USE_RESPONSE_FILE_FOR_OBJECTS OFF) #set(CMAKE_CXX_USE_RESPONSE_FILE_FOR_OBJECTS OFF) set(CMAKE_C_COMPILER [[E:\Tools\Develop\ToolsKits\TI\C5500_Code_Generation_Tools4.4.1\bin\cl55.exe]]) # TI C55x编译器路径 set(CMAKE_CXX_COMPILER ${CMAKE_C_COMPILER}) set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER}) # 链接文件 set(MAP_FILE "${CMAKE_SOURCE_DIR}/Debug/demo.map") set(LINKINFO_XML "${CMAKE_SOURCE_DIR}/Debug/demo_linkInfo.xml") # =======添加编译标志======= add_compile_options(-v5509A) add_compile_definitions(c5509a;CHIP_5509A) add_compile_options(--relaxed_ansi --gcc ) add_compile_options(--memory_model=small --ptrdiff_size=16) add_compile_options(--display_error_number --diag_warning=225) # 编译优化 add_compile_options(-O -g) # =======添加链接标志======= add_link_options(--display_error_number --diag_warning=225 -z -m${MAP_FILE} --stack_size=0x200 --heap_size=0x400) add_link_options(--reread_libs --display_error_number --warn_sections) add_link_options(--xml_link_info=${LINKINFO_XML} --rom_model --sys_stacksize=0x200) ## ====调试与预处理配置==== # 为Clangd提供compile_commands.json #set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # 生成编译数据库 #set(CMAKE_VERBOSE_MAKEFILE ON) # 显示详细编译命令
arm-gcc.cmake
# 设置编译工具集 set(CMAKE_C_COMPILER arm-none-eabi-gcc) set(CMAKE_CXX_COMPILER arm-none-eabi-g++) set(CMAKE_ASM_COMPILER arm-none-eabi-gcc) set(CMAKE_AR arm-none-eabi-ar) set(CMAKE_OBJCOPY arm-none-eabi-objcopy) set(CMAKE_OBJDUMP arm-none-eabi-objdump) set(CMAKE_SIZE arm-none-eabi-size) # 设置编译标准 set(CMAKE_CXX_STANDARD 98) set(CMAKE_C_STANDARD 90) #--------------------------编译选项---------------------------- # 为了区别TI工程和GCC工程 add_compile_definitions(__ARM__) add_compile_options(-mcpu=cortex-m4 -mthumb -mthumb-interwork) add_compile_options(-mfloat-abi=hard -mfpu=fpv4-sp-d16) add_compile_options(-ffunction-sections -fdata-sections -fno-common -fmessage-length=0) # 允许预处理器处理汇编文件 add_compile_options($<$<COMPILE_LANGUAGE:ASM>:-x$<SEMICOLON>assembler-with-cpp>) #--------------------------编译优化---------------------------- add_compile_options(-O2 -g) add_compile_options(-finline-functions) # 关闭异常处理 add_compile_options(-fno-exceptions) # 关闭运行时错误处理(可以降低一些ROM占用) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti") # ----------------------调试选项-------------------------- # 开启内联警告,当函数内联失败时,编译器会发出警告。 add_compile_options(-Winline) #---------------------------链接选项-------------------------- # 链接标志:指在编译和链接过程中,传递给链接器(Linker)的选项或参数,和链接选项是一个东西 add_link_options(-mfloat-abi=hard -mfpu=fpv4-sp-d16) add_link_options(-mcpu=cortex-m4 -mthumb -mthumb-interwork) add_link_options(-Wl,-gc-sections,--relax,--as-needed,--print-memory-usage,-Map=${CMAKE_BINARY_DIR}/${PROJECT_NAME}.map) # 添加链接脚本 add_link_options(-T${CMAKE_SOURCE_DIR}/Tools/scripts/C55x_GCC_LinkerScript.ld) # =====================设置项目文件===================== set(CMAKE_C_COMPILER_WORKS ON) # 强制绕过编译器检查 set(CMAKE_CXX_COMPILER_WORKS ON) #set(CMAKE_C_COMPILER_FORCED ON)
arm-gcc.cmake这里就是直接照搬stm32工程的cmake配置,不过编译过程遇到一个相当奇怪的问题,工程添加C++选项后那么cmake测试时C++编译器就会报系统调用缺失的错误,这点我完全无法理解,工程明明差不多,cmake配置大部分也是照搬原有的stm32工程。
尝试了许久,最后干脆根据报错cmake的条件,把这两个变量强行置为1或者ON,让它强制通过编译器检查即可。这点我是相当无法理解的,简单测试中说编译器有问题,但实际编译根本就没问题。纯粹是让人红温的,想做编译器检查偏偏又没做好
TI工程需要添加编译器库,而GCC不需要,那么需要根据编译器类型来选择性链接库和库目录
set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_VERSION 1) cmake_minimum_required(VERSION 3.29) # 不知道为什么g++编译器测试时会一堆他宝贝的未定义错误 include(Tools/cmake/arm-gcc.cmake) project(demo C CXX ASM) # ===================设置工程目录====================== set(APP_DIR App/demo) # ===================设置工程目录====================== # =====================设置项目文件===================== if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set(TARGET_FILE ${PROJECT_NAME}.elf) else () set(TARGET_FILE ${PROJECT_NAME}.out) endif () #-------------------------------------------库------------------------------------------- file(GLOB_RECURSE SOURCES "${APP_DIR}/*.c" "BSP/src/*.c" "Core/Drivers/src/*.c" ) include_directories( ${APP_DIR} "App" "App/conf" "BSP/inc" "Core/Drivers/inc" ) add_executable(${TARGET_FILE} ${SOURCES}) ## 链接库 # 如果不是GCC,那么就链接TI的库 if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU") target_include_directories(${TARGET_FILE} PRIVATE "E:/Tools/Develop/ToolsKits/TI/C5500_Code_Generation_Tools4.4.1/include" ) target_link_directories(${TARGET_FILE} PRIVATE "E:/Tools/Develop/ToolsKits/TI/C5500_Code_Generation_Tools4.4.1/lib" ) endif () # 添加自定义命令: #add_custom_command(TARGET ${TARGET_FILE} POST_BUILD # COMMENT "Building " #)
链接脚本
链接脚本这块是模仿VC5509.cmd编写的,添加这个是想要在编译后大致打印一下内存布局,大概判断内存占用了多少。之所以是不确定是因为编译的是ARM架构的代码,并非C55x架构。
链接脚本按需修改
ENTRY(_start) /* 指定入口点为启动文件中的_start符号 */ MEMORY { /* 程序存储器 (PMEM) - 通常为片上RAM或外部存储器 */ PMEM (rx) : ORIGIN = 0x00010000, LENGTH = 64K /* 假设程序存储器起始地址 */ /* 数据存储器 (DMEM) - 分多个物理bank */ DARAM (rwx) : ORIGIN = 0x00020000, LENGTH = 64K /* 双访问RAM */ SARAM (rwx) : ORIGIN = 0x00030000, LENGTH = 192K /* 单访问RAM */ /* 外设寄存器空间 (需根据实际映射调整) */ IO_REGS (rw) : ORIGIN = 0x00C00000, LENGTH = 1M } SECTIONS { /* 中断向量表 (需确认C55x向量表位置) */ .intvecs : { KEEP(*(.intvecs)) /* 保持向量表不被优化 */ } > PMEM /* 可执行代码段 */ .text : { *(.text) /* 主程序代码 */ *(.text.*) /* 函数级链接时可能需要 */ KEEP(*(.init)) /* 系统初始化代码 */ KEEP(*(.fini)) /* 系统终止代码 */ } > SARAM /* 常量数据 (ROM中) */ .rodata : { *(.rodata) *(.rodata.*) } > DARAM /* 初始化数据 (加载在PMEM,运行时拷贝到DARAM) */ .data : AT (ADDR(.rodata) + SIZEOF(.rodata) + SIZEOF(.ARM.exidx)) { _data_load = LOADADDR(.data); /* 记录加载地址 */ _data_start = .; *(.data) *(.data.*) _data_end = .; } > DARAM /* 未初始化数据 (BSS段) */ .bss (NOLOAD) : { __bss_start__ = .; *(.bss) *(.bss.*) *(COMMON) __bss_end__ = .; } > DARAM /* 堆空间 (通常放在剩余DARAM) */ .heap (NOLOAD) : { _heap_start = .; . += 0x0400; /* 最小4KB堆 */ _heap_end = .; } > DARAM /* 堆栈 (通常使用SARAM) */ .stack (NOLOAD) : { _stack_top = .; . += 0x0200; /* 2KB系统堆栈 */ _stack = .; } > DARAM /* 特殊功能寄存器 */ .io_regs : { KEEP(*(.io_regs)) /* 如果代码中使用寄存器段声明 */ } > IO_REGS /* 调试信息 (不影响执行) */ .debug_info 0 : { *(.debug_info) } .debug_line 0 : { *(.debug_line) } .debug_abbrev 0 : { *(.debug_abbrev) } .debug_frame 0 : { *(.debug_frame) } } /* 符号导出供启动代码使用 */ PROVIDE(__stack = _stack); PROVIDE(_system_stack_size = 0x0200);
切换GCC工程
首先在CMakeLists这里包含arm-gcc这个子cmake
然后切换为arm-gcc的工具链
这个工具链就是以前开发stm32单片机的那个,可以在本篇开头那篇博客里看到如何配置
再把自定义编译器配置给关闭了
兼容GCC环境
为了能让原本的TI工程既可以兼容GCC,又不会让cl55编译出现问题,那么可以通过条件预编译来控制。比如ioport在cl55里是个关键字,但在arm-gcc里却是未定义标识符,那么可以通过下面这个条件预编译来定义这个宏,防止报错。
注意不要使用__GNUC__这个宏作为判断依据,因为TI开始GCC编译器扩展之后就默认定义了这个宏
// ======================兼容GCC====================== #ifdef __ARM__ // 定义关键字 #define ioport #endif // ======================兼容GCC======================
编译过程可能会报一些缺失函数定义的错误,那么就在一个源文件里随便定义一下
// // Created by fairy on 2025/3/1 16:11. // // 头文件 #include<zq_conf.h> // 为了兼容GCC #ifdef __ARM__ #include <reent.h> #include <sys/stat.h> /* Variables */ extern int __io_putchar(int ch) __attribute__((weak)); extern int __io_getchar(void) __attribute__((weak)); /* Functions */ void initialise_monitor_handles() { } int _getpid(void) { return 1; } int _kill(int pid, int sig) { return -1; } void _exit(int status) { while (1) {} } __attribute__((weak)) int _read(int file, char *ptr, int len) { return len; } __attribute__((weak)) int _write(int file, char *ptr, int len) { return len; } int _close(int file) { return -1; } int _fstat(int file, struct stat *st) { return 0; } int _isatty(int file) { return 1; } int _lseek(int file, int ptr, int dir) { return 0; } int _open(char *path, int flags, ...) { return -1; } int _wait(int *status) { return -1; } int _unlink(char *name) { return -1; } int _times(struct tms *buf) { return -1; } int _stat(char *file, struct stat *st) { return 0; } int _link(char *old, char *new) { return -1; } int _fork(void) { return -1; } int _execve(char *name, char **argv, char **env) { return -1; } void *_sbrk(ptrdiff_t incr) { return (void *)0; } #endif
编译测试
可以看到下面编译过程相当顺利,还能打印一个大概的内存布局。
(图中内存布局是以前遗留的,应为64K和192K)
在头文件这里,可以看到代码已经着色了,说明代码检查已经生效了,能准确判断各个代码是干什么的了
还有我最心心念念的代码补全功能
如果没有生效,那么重启CLion。如果还没有生效,那么一定是哪个地方的配置没到位或者CLion没有更新(这个算是邪法,更新了之后问题就解决了)。
与CCS协同
移动目录 【不推荐】
为了不让变成arm-gcc模样的TI工程影响到CCS发挥,那么我们需要把Tools目录迁移到demo同级目录,不然CCS就会编译里面的.ld文件。
同时为了能让我们在demo这个目录里能看到Tools目录,我们可以在包含头目录这里偷偷包含Tools目录。那么Tools就会显示在外部库这里,可以随时便于我们修改,同时又不会影响CCS编译。
同时include那里也要修改目录为../Tools/cmake/arm-gcc.cmake
与此同时,另一边......
CCS的编译也没有受到任何影响。
可惜的是Tools目录就没法添加到git仓库里了,真是两难的境地了。
隐藏目录
另一种方法是把目录的名称加上前缀“.”,那么目录就是隐藏状态,CCS不会识别出来,但CLion会
排除目录【推荐】
还有一种方法是右键目录,直接把目录排出构建体系
内存布局比较
最后比较一下同样的代码,编译后的二进制文件有什么不同
(左图DARAM与SARAM总大小应为64K和192K)
可以看到,arm-gcc链接的库体积稍大一些,不过总体上已经很接近了。
十二、源码
十一作为结尾不太好听。
源码更新得不勤,也可能会断更,因为板子会被回收
github:https://github.com/ichliebedich-DaCapo/TMS320VC5509A
gitcode:项目首页 - TMS320VC5509A - GitCode