离大谱,没 commit 就回滚了,居然得重写
原本,没有怎么啰嗦的,没想到,第三次写,就越写越啰嗦了,诸位见谅

# 说明

本文硬件基于 LaunchXL-F28379D 和 逻辑分析仪,软件基于 CCS 10.4 版本, 帮助文档为 C2000 的例程和官方文档

在此说明,这一系列的文章主要是记录我学习思考的过程,可能会啰里啰唆

个人观点,授人以鱼不如授人以渔,最后会给出完整的代码和工程的链接

# 逻辑框图

image

一般来说,一个处理器要配置引脚都会有两种方式:寄存器和库函数

但无论哪一种都需要去理解硬件的架构,虽然这幅框图有些复杂,但是大致的可以看出,下半部分是用于配置输出模式的。从寄存器的名称 也可以看出一些寄存器是用于控制输出值的。例如, GPySETGPyDAT 等。

但是要知道如何配置,还是需要去阅读文档的寄存器部分和其他部分的讲解的注意事项

对应文档地址链接,可惜转跳不到对应章节

# 第一步: GPIO 工作模式配置

在文档的 8.2 节中可以找到 GPIO 各种模式配置的概要和说明image

从文档中可以得知,要将 引脚设置为 通用输入输出模式,需要配置 GPyMUXnGPyGMUXn .

但这里并没有给出 个寄存器 需要如何配置。不过文档最后一句给出了,默认情况下,引脚处于 通用输入输出模式

翻阅文档后,找到了 复用表

image

可以看出 当 GPyMUXn 为 0 时, GPyGMUXn 的值与模式配置无关.

所以我们得到了第一步,将 GPyMUXn 设置为 0

但,表格中 GPyMUXnyn 分别代表什么呢,还是得去查找一下寄存器表

在找寄存器表的时候,发现 GPIO 的寄存器分为两大类, GpioCtrlRegGpioDataReg , 分别用于配置 GPIO 和 控制引脚输出值

image

很显然 GPyMUXn 属于控制寄存器,所以很轻松的就找到了 对应的寄存器

image

从图中我们可以知道, GPyMUXny 代表 GPIO 的分组,不同的芯片引脚数不同,具体多少个分组得看芯片

n 则是 1 或 2, 看地址差值为 2 , 然后从之前的复用表中得到一个引脚占 2 个数据位,而 TMS320 是地址总线和数据总线的位宽为 16, 所以得 2*16/2 = 16, 一个 GPxMUXn 控制 16 个引脚。这只是写文章的时候突然想到的,事实上我们可以直接看后面的说明,GPAMUX1 控制 0~15, 一看就知道是 16 个 (捂脸)

事实上知道这些就可以去配置 GPIO 的工作模式了,但是问了预防万一,还是去看一眼寄存器表为好

image

事实上,也没有什么要注意的.

至此,我们就得到了 GPIO 配置的第一步:将 GpioCtrlReg.GPyMUxn 的对应引脚设置为 0.

# 第二步:GPIO 输入输出配置

image

继续阅读文档,会发现 2, 3 步基本没什么用 (对输出模式而言), ODR 寄存器是在输入模式时配置的,且默认是关闭的,所以直接到第 4 步,也是 8.2 节最后有用的地方了,配置 引脚 的输入输出方向

从文档中可以知道,默认情况下 GPIO 的引脚 是输入的。并且在改变 引脚 为输出模式时,建议先写入要输出的值到 数据寄存器中,输出模式下默认输出的电平都为低电平.

个人觉得官方,让我们先设置输出值是为了防止,配置为输出模式时默认的低电平导致,外部设备出现奇怪的反应。咱们现在是空载所以输出啥电平没啥区别.

然后又到了翻看寄存器的时候了,还是去之前的 GpioCtrReg 寄存器表中查找

image

不得不说 TI 的 Go 和 返回总表 做的是真的让人舒心

image

从寄存器数码中可以得知 当 GPyDIR 为 1 时,引脚为输出模式

至此,我们得到了第二步: GpioCtrlReg.GPADIR 对应的引脚置位为 1

# 第三步:输出模式配置

到了这里,8.2 节没有继续说明和单纯的输出模式相关的信息了,但是根据我们之前的配置经验可以知道 还有输出模式需要配置

输出模式分为开漏和推挽输出,问题是这个寄存器叫啥呢,这就需要从框图中查看了,从框图中可以看出 这是一个名为 GPyODR 的寄存器。这就要再次开始翻找 GpioCtrlReg 的寄存器列表

1673011610331

由于 TI 的人性化设计,还是很快就能找到的,从文档中可知 当 GPyODR 为 0 的时候,为正常输出.

至此我们就得到了配置引脚为输出的第三步:将 GpioCtrlReg.GPyODR 对应的 引脚位复位

image

# 小结

到此我们就知道了,引脚配置为输出模式所需要的步骤:

  1. 配置引脚为 通用输入输出模式
  2. 设置引脚输出值
  3. 配置引脚为 输出模式
  4. 配置引脚为 推挽输出

接下来用两种方式来实现 GPIO 配置
这里假设 我们要将 GPIO24 配置为输出模式并输出 5 个方波
不过需要注意的是导入的空文件工程需要不同可能会导致 库函数不同,直接操作寄存的方式也不同
C2000Ware_4_01_00_00\driverlib 路径下导入的工程,寄存器操作会麻烦很多,但是库函数资料多
而从 C2000Ware_4_01_00_00\device_support 路径下导入则是寄存器操作更加贴近手册、便于操作,库函数体系自成一家
简单查看一下源文件就看可以知道两个工程的风格相差甚远 (说实话,两种库的底层构建都给我极大的启发)
下面我会用两个库分别实现寄存器版本和库函数版本的操作

# device_support

# 寄存器版本

这个当初我是直接翻看,源码看明白的,大致讲一下思路

文档中给出了两个寄存器的名称,然后我直接输进去然后出现自动补全,然后就没有然后了

不过,在输入寄存器名称的时候,我跳转到源码查看了一下

image

找到了对应的结构体,翻阅了一下成员,发现与文档相符然后就直接操作了, GpioDataReg 也是同样的道理image

根据上面的小结我们可以得到一下代码

/**
 * @b 寄存器配制引脚
 */
void Reg_GpioCfg(void)
{
    EALLOW;
    GpioDataRegs.GPADAT.bit.GPIO24 = 0;      	// 预设 pin 输出电平
    GpioCtrlRegs.GPAMUX2.bit.GPIO24 = 0;	// 设置 pin 为通用输入输出模式
    GpioCtrlRegs.GPADIR.bit.GPIO24 = 1;		// 设置 pin 为输出
    GpioCtrlRegs.GPAODR.bit.GPIO24 = 0;		// 设置 pin 为推挽输出
    EDIS;
}

EALLOW 是用于解除写保护, EDIS 是用于恢复写保护的,这个在寄存器表中可以得到

接下去我们要实 IO24 输出 5 个高电平,这个就比较简单啦,无非就是写个随机延时 + 上 for 循环,用 GPADAT 或者 GPASETGPACLEAR 以及 GPATOGGLE 来操作

这里,我选择用 GPATOGGLE 直接操作,能少些好多行代码

/**
 * @b 测试函数:产生 5 个方波
 */
void test_function(void)
{
    for (volatile int i = 0; i < 10; i++)
    {
        GpioDataRegs.GPATOGGLE.bit.GPIO24 = 1; // 翻转电平
	// 随机延时
        int j = 1000;
        while (j--)
            ;
    }
}

# 库函数版本

由于没找到相关的文档,就只能直接读源码了
image
直接翻阅 Gpio.c 文件,就可以看到和 GPIO 配置相关的代码了
一顿翻看函数注释后就可以知道 GPIO_SetupPinMuxGPIO_SetupPinOptions 可以完成对 引脚的配置
image
image
GPIO_SetupPinMux 是用于 将 引脚设置位 通用 I/O 模式, GPIO_SetupPinOptions 则是用于选择 输入输出和输出模式

由此可以得到最终代码

/**
 * @b 函数配置 引脚
 */
void Fun_GpioCfg(void)
{
    GPIO_SetupPinMux(24, GPIO_MUX_CPU1, 0);                 // 31 脚配置为 GPIO
    GPIO_SetupPinOptions(24, GPIO_OUTPUT, GPIO_PULLUP);     // 31 脚配置为 推挽输出
}

# driverlib

由于这个的寄存器版本,会涉及到阅读库函数,所以先讲库函数

# 库函数版本

翻阅文档在 8.9.4 小结可以看到对应的 操作函数

image

当然也可以去官网下载 F2837xD_DriverLib_Users_Guide.pdf , 这个文档 分模块的介绍了各个 API 和该模块对应的结构体

image

或者直接查看 device.h 头文件中包含的 gpio.h 文件,里面对各个函数的用途及其参数进行了详细的说明
image

途径很多,就我的习惯而言,会先从文档中获取相关函数,然后再去代码注解中去配置函数如何使用,等实现效果后再回头分析 这个函数的底层的实现
所以,这我也按这种方式介绍.

首先,第一步需要操作 GPyMUXn , 从文档的 8.9.4 节可知,需要查看 GPIO_setPinConfig ,
查看源码得到,这个函数只需要 将要配置的引脚 的引脚号传入即可 GPIO_setPinConfig(24);
1673098106423

第二步,需要操作设置 引脚要输出 的值,我想输出 低电平,默认就是低电平,所以不需要配置
不过需要一开始输出高电平则,从文档中可知 可以使用 GPIO_writePinGPIO_setPortPins 来将电平拉高
查看源码注释可知配置为高电平的方式 GPIO_writePin(24, 1)GPIO_setPortPins(GPIO_PORT_A, (uint32_t)1 << 24)

第三步,将 引脚 配置为输出模式,从文档得 需要操作 GPIO_setDirectionMode
查看源码可知,函数需要配置两个参数:引脚号和输出模式。从注释中看可以知道,输出模式有宏定义,所以可以直接套用宏定义
得到源码 GPIO_setDirectionMode(24, GPIO_DIR_MODE_OUT);
image

最后一步,配置引脚的输出模式,从文档中可知,需要操作 GPIO_setPadConfig
查看源码后返现,这个函数需要两个参数:引脚号和输入 | 输出模式引脚状态
有意思的是,这个函数 可以进行 输出模式的推挽开漏配置 和 输入模式的上拉和下拉配置.
通过 宏来进行区分,当前引脚需要配置什么.(真就没有防护)
得到代码 GPIO_setPadConfig(24, GPIO_PIN_TYPE_STD);
image

最后得到对应的源码如下

/**
 * @b 初始化 pin 为输出模式
 */
void Fun_setupOutputPin(uint32_t pin)
{
    GPIO_setPinConfig(pin);                          // 配置为 通用输入输出模式
    GPIO_setDirectionMode(pin, GPIO_DIR_MODE_OUT);   // 配置为 输出模式
    GPIO_setPadConfig(pin, GPIO_PIN_TYPE_STD);       // 配置为 推挽输出
}

# 寄存器版本

这个库里的寄存器就没有特定的结构体了,有的是大量的枚举和强制转换,比较考验指针操作和对寄存器的熟悉程度
所以我们就需要去看一下库函数是如何实现的
image

从源码中可以看出,这的寄存器操作是通过 GPIO 的基地址强转为 指针,通过访问数组的方式来实现修改值
相关的宏 分布在 gpio.hhw_gpio.h 以及 hw_memmap.h 中定义,具体情况自行查看

这里就直接贴出源码,因为纯粹的翻看寄存器 和 算内存偏差实在是没什么可以说的了
注释里已经写的很清楚了

/**
 * @b 初始化 pin 为输出模式
 */
void Reg_setupOutputPin(uint32_t pin)
{
    uint32_t pinMask = 0;                   // 其他 1 位寄存器掩码
    uint32_t muxPinMask = 0;                // MUX 寄存器掩码
    volatile uint32_t *gpioCtrlReg = NULL;  // 指向 gpioCtrlReg 的 指针
    // 定位 GPIOA 所有寄存器的首地址,GPIO_CTRL_REGS_STEP 是寄存器步长,一组 GPIO 寄存器的长度
    gpioCtrlReg = (uint32_t*) ((uintptr_t) GPIOCTRL_BASE
            + (pin / 32) * GPIO_CTRL_REGS_STEP);
    // 要被置位的引脚掩码计算
    pinMask = (uint32_t) 1U << (pin % 32);
    // 一个 MUX 管理 16 个引脚,所以 对 16 取余数
    // 乘 2 是因为一个引脚对应两个位,每过一个引脚要 移两位
    muxPinMask = ((uint32_t) 3U << ((pin % 16) * 2));
    EALLOW;
    // 设置为输出模式
    gpioCtrlReg[GPIO_GPxDIR_INDEX] |= pinMask;
    // 设置为推挽输出
    gpioCtrlReg[GPIO_GPxODR_INDEX] &= ~pinMask;
    // 设置位 通用输出模式
    if ((int)(pin % 32) - 16 >= 0)    // 判断引脚属于 MUX1 还是 MUX2
    {// 为什么 + 1 而不是 +2 我也没弄懂,挠头,等弄懂再更新
        gpioCtrlReg[GPIO_GPxMUX_INDEX + 1] &= ~muxPinMask;
    }
    else
    {
        gpioCtrlReg[GPIO_GPxMUX_INDEX] &= ~muxPinMask;
    }
    EDIS;
}

# 代码实现

# device_support

使用 C2000Ware_4_01_00_00\device_support 空工程 导入 实现的源码

#include "F28x_Project.h"
void Fun_GpioCfg(void);
void Reg_GpioCfg(void);
void test_function(void);
void main(void)
{
    InitSysCtrl();  // 初始化系统配置,(例如配置时钟,关闭看门狗...)
    InitGpio();     // 初始化 GPIO 模块
    // 关中断
    DINT;
    InitPieCtrl();      // 初始化中断控制器
    InitPieVectTable(); // 初始化中断向量表
    // 开中断
    EINT;
    // 初始化 GPIO, 二选一即可
    Reg_GpioCfg();
    //Fun_GpioCfg();
    test_function();
    while (1)
    {
    }
}
/**
 * @b 测试函数:产生 5 个方波
 */
void test_function(void)
{
    for (volatile int i = 0; i < 10; i++)
    {
        GpioDataRegs.GPATOGGLE.bit.GPIO24 = 1;
        int j = 1000;
        while (j--)
            ;
    }
}
/**
 * @b 函数配置 引脚
 */
void Fun_GpioCfg(void)
{
    GPIO_SetupPinMux(24, GPIO_MUX_CPU1, 0);                 // 31 脚配置为 GPIO
    GPIO_SetupPinOptions(24, GPIO_OUTPUT, GPIO_PULLUP);     // 31 脚配置为 推挽输出
}
/**
 * @b 寄存器配制引脚
 */
void Reg_GpioCfg(void)
{
    EALLOW;
    GpioCtrlRegs.GPAMUX2.bit.GPIO24 = 0;
    GpioCtrlRegs.GPADIR.bit.GPIO24 = 1;
    GpioCtrlRegs.GPAODR.bit.GPIO24 = 0;
    EDIS;
}

# driverlib

#include "driverlib.h"
#include "device.h"
void Reg_setupOutputPin(uint32_t pin);
void Fun_setupOutputPin(uint32_t pin);
void test_function(void);
//
// Main
//
void main(void)
{
    // 系统初始化
    Device_init();
    // Reg_setupOutputPin(24);
    Fun_setupOutputPin(24);
    // 测试函数
    test_function();
    while (1)
        ;
}
/**
 * @b 初始化 pin 为输出模式
 */
void Fun_setupOutputPin(uint32_t pin)
{
    GPIO_setPinConfig(pin);                          // 配置为 通用输入输出模式
    GPIO_setDirectionMode(pin, GPIO_DIR_MODE_OUT);   // 配置为 输出模式
    GPIO_setPadConfig(pin, GPIO_PIN_TYPE_STD);       // 配置为 推挽输出
}
/**
 * @b 初始化 pin 为输出模式
 */
void Reg_setupOutputPin(uint32_t pin)
{
    uint32_t pinMask = 0;                   // 其他 1 位寄存器掩码
    uint32_t muxPinMask = 0;                // MUX 寄存器掩码
    volatile uint32_t *gpioCtrlReg = NULL;  // 指向 gpioCtrlReg 的 指针
    // 定位 GPIOA 所有寄存器的首地址,GPIO_CTRL_REGS_STEP 是寄存器步长,一组 GPIO 寄存器的长度
    gpioCtrlReg = (uint32_t*) ((uintptr_t) GPIOCTRL_BASE
            + (pin / 32) * GPIO_CTRL_REGS_STEP);
    // 要被置位的引脚掩码计算
    pinMask = (uint32_t) 1U << (pin % 32);
    // 一个 MUX 管理 16 个引脚,所以 对 16 取余数
    // 乘 2 是因为一个引脚对应两个位,每过一个引脚要 移两位
    muxPinMask = ((uint32_t) 3U << ((pin % 16) * 2));
    EALLOW;
    // 设置为输出模式
    gpioCtrlReg[GPIO_GPxDIR_INDEX] |= pinMask;
    // 设置为推挽输出
    gpioCtrlReg[GPIO_GPxODR_INDEX] &= ~pinMask;
    // 设置位 通用输出模式
    if ((int)(pin % 32) - 16 >= 0)    // 判断引脚属于 MUX1 还是 MUX2
    {// 为什么 + 1 而不是 +2 我也没弄懂,挠头,等弄懂再更新
        gpioCtrlReg[GPIO_GPxMUX_INDEX + 1] &= ~muxPinMask;
    }
    else
    {
        gpioCtrlReg[GPIO_GPxMUX_INDEX] &= ~muxPinMask;
    }
    EDIS;
}
/**
 * @b 测试函数:产生 6 个方波
 */
void test_function(void)
{
    uint32_t pin = 24;
    uint32_t pinMask = 0;
    volatile uint32_t *gpioDataRegs = NULL;
    gpioDataRegs = (uint32_t*) ((uintptr_t) GPIODATA_BASE + (pin / 32) * 0x8);
    pinMask = (uint32_t) 1U << (pin % 32);
    for (volatile int i = 0; i < 12; i++)
    {
        gpioDataRegs[GPIO_GPxTOGGLE_INDEX] |= pinMask;
        int j = 1000;
        while (j--)
            ;
    }
}

# 现象

产生 6 个方波

image

# 工程链接

由于 device_support 移动后依赖有点难修复,所以工程文件中只有 driverlib 的工程链接
🐱(⬅️点我)


大道五十,天衍四十九,人遁其一!