FMD初探

前言

  • 之前学习过51单片机,故寻找一个类似的8位单片机进行开发,FMD是国产的可擦写、RISC-V架构的8Bit单片机,价格低廉的同时具备比较丰富的外设,使用场景大都在小家电产品、太阳能控制板等小型程序上。

1. 概述

1.1 学习环境

  • 硬件资源:FT61F02X

    • 这是一款8bit基于EEPROM的RISC-V MCU
    • PROGRAM:2k * 14 bit
    • DATA:256 * 8 bit
    • RAM:128 * 8 bit

1.2 学习任务

  • 了解MCU的基本初始化配置及启动流程。
  • 成功电亮第一盏LED灯。
  • 完成XC-RD-5LED-V2的所有功能。
    1. 5枚LED电源指示灯
    2. 按键控制
    3. 红外接收
    4. 雷达感应

2. System Init

2.1 OSCCON寄存器配置 - 初始化系统时钟

  • 地址 名称 bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0 复位值
    0x8F OSCCON LFMOD IRCF[2] IRCF[1] IRCF[0] OSTS HTS LTS SCS 0101 x000

    一般情况下,我们要关注的是OSCCON寄存器中的IRCF[2:0]位SCS位。SCS位是OSCCON中的最低位,决定了MCU使用内部晶振(SCS == 1)还是外部晶振(SCS == 0,而3bit IRCF用来对内部高速/低速时钟进行分频,此外LFMOD是低速内部时钟的频率选择位,当LFMOD == 1时,内部低速时钟速率为:256kHz,当LFMOD == 0时,内部低速时钟速率为:32kHz。

    一般的,若不选择外部时钟,都优先考虑选择内部高速时钟,故:

    1
    2
    LFMOD = 0;   //不选择LIRC作为时钟源
    IRCF = 111; //配置HIRC的时钟频率为16Mhz,其它配置请参照手册 表6-2

由此可知OSCCON的值应为:OSCCON = 0B01110001

2.2 GPIO寄存器配置 - 初始化GPIO

  • 地址 名称 bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0 复位值
    0x85 TRISA TRISA[7] TRISA[6] TRISA[5] TRISA[4] TRISA[3] TRISA[2] TRISA[1] TRISA[0] 1111 1111
    0x87 TRISC None None TRISC[5] TRISC[4] TRISC[3] TRISC[2] TRISC[1] TRISC[0] ++11 1111
    0x05 PORTA PORTA[7] PORTA[6] PORTA[5] PORTA[4] PORTA[3] PORTA[2] PORTA[1] PORTA[0] xxxx xxxx
    0x07 PORTC None None PORTC[5] PORTC[4] PORTC[3] PORTC[2] PORTC[1] PORTC[0] ++xx xxxx
    0x95 WPUA WPUA[7] WPUA[6] WPUA[5] WPUA[4] WPUA[3] WPUA[2] WPUA[1] WPUA[0] 1111 1111
    0x88 WPUC None None WPUC[5] WPUC[4] WPUC[3] WPUC[2] WPUC[1] WPUC[0] ++11 1111
    0x89 WPD None None None WPDA WPDC1 WPDC2 WPDC3 None +++0 000+
    0x81 OPTION /PAPU INTEDG T0CS T0SE PSA PS2 PS1 PS0 1111 1111

    SOP16封装的FT61F02X芯片总共引脚是16枚,除去VCC和GND剩下就是14枚,PORTA+PORTC[5:0]共14bit作为数据输出寄存器通过配置寄存器的值向外输出数据,相应地WPUx寄存器则是控制端口是否处于弱上拉状态(WPUx[i] = 1; 为弱上拉,反之弱下拉),在配置上拉状态之前,由TRISx寄存器控制IO的方向,TRISx[i] = 1 是输入,反之为输出

    特别地,FT61F02X的每个GPIO引脚都可以配置成弱上拉,但弱下拉不是每个引脚都可以配置的,但是都是通过WPD寄存器配置的

    • 弱下拉PORTA中只有bit4可以被配置,涉及到的寄存器WPD中:WPDA4
    • PORTC中,bit1~bit3都可以被配置,涉及到的寄存器WPD中:WPDC[3:1]

    OPTION寄存器是中断相关寄存器,其作用如下表:

  • 地址 bit位号 名称 释义 作用
    0x81 bit7 /PAPU 1 时,关闭所有PORTA上拉功能, 为 0 时,上拉由WPUA控制
    0x81 bit6 INTEDG Init PC1 Edge Interrupt Way 初始化PC1的边沿中断,为 1 时,配置为上升沿, 反之下降沿
    0x81 bit5 T0CS Timer 0 Change Clock Source 选择定时器0的时钟源,为 1 时,由PA2/T0CKI(作计数器)提供, 为 0 时,由指令时钟(作定时器)提供
    0x81 bit4 T0SE Timer 0 Select Edge Interrupt Way 选择定时器边沿触发方式,为 1 时,下降沿, 反之上升沿
    0x81 bit3 PSA Prescaler Allocated 1 时,分频电路分配给WDT后分频器,反之Timer0的预分频器
    0x81 bit2 PS2 Prescaler bit2 PS2\PS1\PS0都是PS,这3个bit用于配置WDT或Timer0的分频比, 具体查看手册 表7-5
    0x81 bit1 PS1 Prescaler bit1 PS2\PS1\PS0都是PS,这3个bit用于配置WDT或Timer0的分频比, 具体查看手册 表7-5
    0x81 bit0 PS0 Prescaler bit0 PS2\PS1\PS0都是PS,这3个bit用于配置WDT或Timer0的分频比, 具体查看手册 表7-5

    由此可知,根据本任务的要求GPIO的配置如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    INTCON = 0; //暂时禁止所有中断

    PORTA = 0B00000000;
    TRISA = 0B01010011;

    PORTC = 0B00000000;
    TRISC = 0B00100000;

    WPUA = 0B00000000;
    WPUC = 0B00000000;

    OPTION = 0B00000000;

2.3 MSCKCON寄存器配置

  • 地址 名称 bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0 复位值
    0x1B MSCKCON None VREG_OE T2CKSRC SLVREN None CKMAVG CKCNTI None 0000 +00+

    MSCKCON寄存器涵盖了稳压器、Timer2时钟源、睡眠模式LVR控制、LIRC和HIRC交叉校准4次平均模式控制、LIRC和HIRC的交叉校准功能控制,这些功能和上述表单一一对应,写入1就是使能相关功能,反之为关闭。

    值得注意的是,CKCNTI是双晶振校准是否完成的判别位(FLAG),在完成交叉校准后此位会自动清零

    此外,SLVREN位仅适用于 LVREN 配置成由指令 SLVREN 控制 LVR时,此位有效,通过改变SLVREN的值,控制LVR的开关。

    T2CKSRC位用于选择时钟源,并且默认为2倍频,例如当其为1时,Timer2的时钟源为HIRC的2倍(ECCP模式生效),为0时,是指令时钟的2倍。

    VREG_OE用于使能PA4和PC5是否作为稳压器的输出引脚。

2.4 CMCONx,x = [0, 1] 寄存器配置

  • CMCONx寄存器有两个,用于配置C1,C2比较器的输出结果、模式和输入切换,具体配置参考手册 表13-1

3. 调试

  • 使用FMD IDE可以实现类似于Keil的功能,配合FMD Link可以对FMD所有的8位机系列的硬件进行调试、仿真和开发。

3.1 注意事项

  1. FMDIDE的编译器是GCC,但是不能直接用网络上的,其C语言编译器放在IDE安装目录下:{安装目录}\data\bin\c.exe

  2. 注意选对芯片的型号。

C指针

I. 目录

1. 指针概述

1.1 基本概念

  1. 指针变量的内容是某变量(数组)的地址(首地址)

  2. 对*p进行自增(自减)操作时,其按照声明的对应变量类型的大小进行自增(自减)

  3. & - 取地址, * - 解引用

  4. 指针是可以多重嵌套的,由此引申出多重指针,如下示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int       a = 5;
    //类似于积分
    int* p = &a;
    int** p0 = &p;
    int*** p1 = &p0;
    //类似于求导
    *p1 == &p;
    *(*p1) == &a;
    *(*(*p1)) == 5;
  5. 程序内存的分布主要为四大块,堆、栈、Static/Global、Code

    Code - 代码的存储空间

    Static/Global - 全局变量的存储空间

    Stack - 局部变量、程序运行时的函数地址、变量参数等的存储空间

    Heap -

1.2 注意事项

  1. 声明的指针类型要与所保存地址的变量类型一致,否则编译器会报错,需要强制类型转换。

  2. 强制类型转换会导致数据的截断。

  3. int* p = &a;
    这个方式的*并不是解引用,而是声明指针变量并初始化。

  4. 指针的类型除了对应变量的类型之外,还有void

1.3 使用场景

1.3.1 无返回值,处理main内局部函数

  • 通过设置函数传参为指针变量,那么就可以通过解引用的方式对main函数内的局部变量在另一个函数内处理,而不需要使用return。

  • 优点:

    1. 不需要创建全局变量,节省内存
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /*
    * 功能:通过解引用的方式修改main函数内局部变量的值
    */

    void incA(int *p)
    {
    *p = (*p)++;
    }

    int main()
    {
    int a = 5;
    int* p = &a;

    incA(p);

    printf("After incA the value of a is: %d \r\n");
    }

2.数组和指针

2.1 一般数组和指针

  • 数组A[i]其中直接A保存的是数组的首地址,因此可以对其自增,自增的结果即是按变量类型进行地址的偏移

    &A[i] == A+i 数组元素的地址可以通过指针偏移得到

    A[i] == *(A+i) 数组的值也可以通过指针偏移解引用得到

  • 注意不要用A++实现数组偏移,因为声明了数组A之后,其变量名为const类型,不允许自增,可以A[i++],此外还可以用*p = A,再 p++,这样是合法的。

  • 数组传参到另一个函数的时候,在堆栈上的体现并不是像变量一样拷贝了一份,而是仅拷贝了数组的首地址指针。
    也就是说传数组其实是传引用(指针)
    那么函数参数可以写成int* A而不是int A[]

2.2 字符数组和指针

  • 基本认识 - 字符串数组末端要有\0,现代某些编译器会自动加,如果不会就需要自己处理

深刻理解数组不能自增寻址,而用指针进行间接自增寻址

例如 char A[10] 不能直接 A++ 得到下一个元素,而可以通过 char *C = A -> C++ 的方式得到下一个元素

  • 注意char A[20] = "Hello" 虽然和 char *A = "Hello" 最后在Printf函数中打印出来的结果是一致的,但是后者被分配到了常量区,而不是变量区,不可以进行读写 ,也就说明数组是const类型的指针。

2.3 指针和多维数组

  • 1.由上面的数组和指针我们可以知道,数组是const类型的指针集合。

  • 2.通过下面这个例子来了解多维数组和指针和解应用之间的嵌套关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int B[2][3] = {{2,3,6},{4,5,8}};
    int (*p)[3] = B;
    // 此时p就是指向B[0]的地址的指针
    // B会返回一个一维数组
    B == (*p)[3]
    *B == B[0] == &B[0][0]
    // 二维数组B的首地址就是B,也是B[0]所存的内容是个地址,这个地址就是B[0][0]在内存中的位置。
    // 所以对B进行解引用得到的元素是B[0][0]的地址
    // 得到B[0][0]的数值
    *(*B) == B[0][0]

3. 指针和动态内存

3.1 栈

3.1.1 栈基本内容

  • 1. 位于 Code区Static/Global区 之上。
  • 2.程序所有的局部变量和函数调用 都在 栈内完成
  • 3.出栈入栈方式是:先进后出
  • 4.每一个函数入栈后都会分配一个区域,在这个区域内完成其局部变量的数据处理,术语称为:栈帧
  • 5.基本的,main函数分配在栈底,当main调用另一个函数fn()时,会进入中断,此时main函数暂停运行,栈内会分配新的栈帧用于fn(),若fn()还有fn1()…等,那么也会继续向上分配栈帧,并进入中断,当所有的栈帧执行完之后,就会返回到父函数继续执行,执行结束的子函数所拥有的栈帧将随着中断的退出而销毁释放
  • 重点:数据结构的栈实现之后,就是此处讨论的栈,其特性就是先进后出。

3.1.2 栈实际问题

  • 1.程序运行时栈空间就已经被分配成了固定大小
  • 2.若程序中有递归函数或频繁调用函数的时候,会有栈溢出的风险,进而导致程序崩溃
  • 3.变量在栈中创建其地址每一次都是随机的,不受控制,因此当程序涉及到不定长数据存储的问题时候,用栈变量就可能会导致程序崩溃,此时就引出了堆(Heap)

3.2 堆

3.2.1 堆基本内容

  • 相反与栈,堆空间可自由被分配并且保存,不会被自动回收或销毁,需要自己手动通过free()函数进行销毁,否则会一直存在堆上被占用
  • 堆也被称为:内存的空闲池内存空闲存储区动态存储区
  • 数据结构中也有堆的概念,但是与此处不一致
    • 数据结构中的堆是:
    • 此处堆是空闲的内存区域(堆)
  • C语言中操作栈所涉及的函数:
    • malloc()
    • calloc()
    • realloc()
    • free()
  • C++中操作栈所涉及的操作符
    • new
    • delete
  • 堆如果无法通过malloc()得到一块内存,将会返回NULL

3.2.2 堆的实际应用

  • 申请对应变量类型的堆空间
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //申请一个在堆上的变量,记得用free释放
    int *p;
    p = (int*)malloc(sizeof(int));
    ...
    free(p);

    //申请一个在堆上的数组
    int *p;
    p = (int*)malloc(20*sizeof(int));
    ...
    free(p);

    //申请其他类型的堆变量
    void *p;
    p = (char*)malloc(sizeof(char));
    ...
    free(p);

3.3 动态分配函数

  1. malloc - 函数原型 void* malloc(size_t size)
  • 为什么int *p = (int*)malloc(x*sizeof(int))中前面要加int*,这个问题涉及到malloc函数返回值是void型指针,所以要用到类型转换,才能够对指针解引用。
  1. calloc - 函数原型 void* calloc(size_t num, size_t size)
  • calloc函数接收两个参数,一个是要分配的对应变量类型的数量,根据数量划分堆空间,并且初始化为0malloc不会初始化对应变量
  1. realloc - 函数原型 void* realloc(void* ptr, size_t size)
  • 此函数接收两个参数:已分配的指针p,以及新的内存块大小,用于重新分配内存块的大小,分配方式取决于原内存块相邻地址空间是否满足新内存块的大小,满足则拓展,不满足即拷贝到新区域。

3.3.1 应用

  • 在不适用C99的环境下需要用到不定长数组 malloc/calloc
    1
    2
    3
    int n;
    scanf("%d", &n);
    int *A = (int*)malloc(n*sizeof(int));
  • calloc可以做到类似于free的效果,但是取决于编译器

3.4 内存泄漏

  • 注意malloc或者calloc分配的内存空间,要用free释放,否则在栈上调用函数时创建的堆变量会留着,每一次调用函数都将在堆上新建变量,随着时间的推移,堆空间会满,最后溢出,导致程序的崩溃。

4. 函数与指针

4.1 函数返回指针

  1. 栈顶向栈底传参数一般是不允许的,因为会导致数据被覆盖
    • 当main函数调用一个fn()并且这个fn会返回一个指针变量回到main,通常上来说main就能操作这个变量,并且这样用指针进行运算的方式会节省栈空间。但会出现一个重大的问题,如果这个fn()在操作语句中间夹入了其它的fn1(),那么此时fn1()会被压入栈中,上一个指针操作的内容从栈顶向下返回,如果没有及时在main内操作,就会被fn1()重新覆盖栈空间,最终解引用fn()返回的指针就会得到不正确的数据。
  2. 如何解决第一个被覆盖的问题呢?
    • 此时就应用到malloc分配内存函数,将在fn()中需要保存的变量从堆中申请,就可以通过free来控制内存何时被释放,也不会将此内容压入栈区,避免被其它函数入栈时导致此变量被覆盖。
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

int* Add(int *a, *b)
{
int* c = (int*)malloc(sizeof(int));
*c = *a + *b;

return c;
}

void HelloWorldPrt(){
printf("Hello World");
}

void main()
{
int a, b;
a = 5, b = 6;

int *ptr;

ptr = Add(&a, &b);

//如果c没有用malloc在堆中申请内存,而prt有没有被printf及时调用,那么c的值在栈区会因为HelloWorldPrt()函数的入栈导致数值丢失或错乱,最终printf输出得不到11,而会是脏数据。
HelloWorldPrt();

printf("The value after Add is: %d", *ptr);
}

4.2 函数指针

  • 既然指针能保存地址,而函数入栈的时候也会有对应的地址,那么能否利用指针这种数据类型来保存函数的地址呢?并且通过解引用的方式,实现函数的调用。

4.2.1 定义方式

  • 设有一个函数:int Add(int a, int b)
  • 指针函数应为:int (*p)(int, int)

    参数数量以及类型需要一一对应,返回值类型也要一一对应

4.2.2 使用案例

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
void PrintfHello(char *name)
{
printf("Hello %s\n", name);
//为什么name不用解引用呢?
//因为传入的"OneLanp"是指针数组,相当于把首地址赋值给了name指针,如果用解引用的话,得到的就是O
}

int Add(int a, int b)
{
return a+b
}

int main()
{
int (*ptrAdd)(int, int);
void (*ptr)(char*);

ptrAdd = &Add;
ptr = PrintfHello;

ptrAdd(5, 3);
printf("%d \r\n", *ptrAdd); // 最后得到5
ptr("OneLanp"); //输出 Hello OneLanp

return 0;
}

4.3 回调函数

  • 回调函数指的是一个函数fn(),被另一个函数以参数的形式传入fn1(fn),并且在fn1中调用了,那么称fn()为回调函数

4.3.1 使用场景

  • 使得代码更有逻辑性和通用性,以下用qsort()函数来示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    int compare(const void* a, const void *b)
    {
    int A = *((int*)a);
    int B = *((int*)b);

    return A-B;

    //如果compare返回的值小于零,那么元素会被排在左边
    //反之排在右边
    //等于零为不确定

    //因此我只需要通过这个函数判断两个值对比的时候谁放在左边或者右边就可以排序

    //而排序的函数可以用qsort,也可以用自己写的冒泡排序。

    //这就是回调函数的一种用法,低耦合,高内聚。
    }

    int main()
    {
    int i, A[] = {-23, 8, -1, 5, -11, 9};
    qsort(A, 6, sizeof(int), compare);
    for(i = 0; i<6; i++) printf("%d", A[i]);
    }

5.嵌入式C使用案例

5.1 定义寄存器并修改寄存器的值

  1. 要注意编译器支持C的版本,C99可支持变长数组,数组元素可为变量
  2. 源代码经过编译器编译后得到可执行文件将存在ROM中,运行时将把二进制指令拷贝到RAM中进行运行。

第一篇博客

Update Through Github Actions