【Linux驱动开发】PS2游戏手柄驱动开发与输入子系统框架

@[toc]

为什么需要输入子系统框架

我们首先来看字符类驱动框架:

1)写file_operations结构体的成员函数: .open()、.read()、.write()
2)在入口函数里通过register_chrdev()创建驱动名,生成主设备号,赋入file_operations结构体
3)在出口函数里通过unregister_chrdev() 卸载驱动

若有多个不同的驱动程序时,应用程序就要打开多个不同的驱动设备,由于是自己写肯定会很清楚,如果给别人来使用时是不是很麻烦?
所以需要使用输入子系统, 使应用程序无需打开多个不同的驱动设备便能实现。

输入子系统框架

驱动层

将底层的硬件输入转化为统一事件形式,想输入核心(Input Core)汇报。
输入子系统核心层
它承上启下为驱动层提供输入设备注册与操作接口,如:input_register_device;通知事件处理层对事件进行处理;在/Proc下产生相应的设备信息。

事件处理层

主要是和用户空间交互(Linux中在用户空间将所有的设备都当作文件来处理,由于在一般的驱动程序中都有提供fops接口,以及在/dev下生成相应的设备文件nod,这些操作在输入子系统中由事件处理层完成)。
设备描述

input_dev结构是实现设备驱动核心工作:向系统报告按键、触摸屏等输入事件(event,通过input_event结构描述),不再需要关心文件操作接口。驱动报告事件经过inputCore和Eventhandler到达用户空间。

注册输入设备函数:

int input_register_device(struct input_dev *dev)

注销输入设备函数:

void input_unregister_device(struct input_dev *dev)

驱动实现——初始化(事件支持)

set_bit()告诉input输入子系统支持哪些事件,哪些按键。例如:

set_bit(EV_KEY,button_dev.evbit) (其中button_dev是struct input_dev类型)

struct input_dev中有两个成员为:
1)evbit事件类型(包括EV_RST,EV_REL,EV_MSC,EV_KEY,EV_ABS,EV_REP等)。
2)keybit按键类型(当事件类型为EV_KEY时包括BTN_LEFT,BTN_0,BTN_1,BTN_MIDDLE等)。

驱动实现——报告事件

用于报告EV_KEY,EV_REL,EV_ABS事件的函数分别为:
void input_report_key(struct input_dev *dev,unsigned int code,int value)
void input_report_rel(struct input_dev *dev,unsigned int code,int value)
void input_report_abs(struct input_dev *dev,unsigned int code,int value)

驱动实现——报告结束

input_sync()同步用于告诉input core子系统报告结束,触摸屏设备驱动中,一次点击的整个报告过程如下:
input_reprot_abs(input_dev,ABS_X,x); //x坐标
input_reprot_abs(input_dev,ABS_Y,y); // y坐标
input_reprot_abs(input_dev,ABS_PRESSURE,1);
input_sync(input_dev);//同步结束

实例分析(按键中断程序):

//按键初始化

static int __init button_init(void)
{//申请中断
    if(request_irq(BUTTON_IRQ,button_interrupt,0,”button”,NUll))
        return –EBUSY;
    set_bit(EV_KEY,button_dev.evbit); //支持EV_KEY事件
    set_bit(BTN_0,button_dev.keybit); //支持设备两个键
    set_bit(BTN_1,button_dev.keybit); //
    input_register_device(&button_dev);//注册input设备
}

/*在按键中断中报告事件*/
Static void button_interrupt(int irq,void *dummy,struct pt_regs *fp)
{
    input_report_key(&button_dev,BTN_0,inb(BUTTON_PORT0));//读取寄存器BUTTON_PORT0的值
    input_report_key(&button_dev,BTN_1,inb(BUTTON_PORT1));
    input_sync(&button_dev);
}

总结:input子系统仍然是字符设备驱动程序,但是代码量减少很多,input子系统只需要完成两个工作:初始化和事件报告(这里在linux中是通过中断来实现的)。

关于事件报告的实现方法

中断实现 常用于实体按键

按键输入示例:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/gpio.h>
#include <linux/input.h>
#include <linux/delay.h>
#include <linux/slab.h>


#include <../arch/arm/mach-mx28/mx28_pins.h>


#define DEVICE_NAME  "input_key"

struct input_dev *inputdev;

struct imx28x_key_struct {
    int key_code;  /* 按键能产生的键值*/
    int gpio;  /* 按键连接的 GPIO */
    struct work_struct work;  /* 按键的工作队列  */
};

struct imx28x_key_struct keys_list[] ={
    {.key_code = KEY_A, .gpio = MXS_PIN_TO_GPIO(PINID_LCD_D17)},
    {.key_code = KEY_B, .gpio = MXS_PIN_TO_GPIO(PINID_LCD_D18)},
    {.key_code = KEY_C, .gpio = MXS_PIN_TO_GPIO(PINID_SSP0_DATA4)},
    {.key_code = KEY_D, .gpio = MXS_PIN_TO_GPIO(PINID_SSP0_DATA5)},
    {.key_code = KEY_E, .gpio = MXS_PIN_TO_GPIO(PINID_SSP0_DATA6)}
};

static irqreturn_t imx28x_key_intnerrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    int i = (int)dev_id;
    int gpio = keys_list[i].gpio; /* 获取按键的 GPIO */
    int code = keys_list[i].key_code; /* 获取按键的键值  */
    /*
    * 延迟 20uS,看按键是不是按下,如果不是,就是抖动
    */
    printk(KERN_INFO "%d was pressed",code);
    udelay(20);
    if (gpio_get_value(gpio)) {
    return IRQ_HANDLED;
    }
    input_report_key(inputdev, code, 1);  /* 先报告键按下事件 */
    input_sync(inputdev);
    schedule_work(&(keys_list[i].work));  /* 提交工作队列,实现中断的下半部处理 */
    return IRQ_HANDLED;
}


static void imx28x_scankeypad(struct work_struct *_work)
{
    /* 通过工作队列指针而获得它所属的 imx28x_key_struct 类型的对象 */
    struct imx28x_key_struct *key_tmp = container_of(_work, struct imx28x_key_struct, work);
    int gpio = key_tmp->gpio;
    int code = key_tmp->key_code;
    /* 每隔 10mS 检查按键是否已经提起,如果没有提起就一直等待 */
    while(!gpio_get_value(gpio)){ 
    mdelay(10); 
    }
    input_report_key(inputdev, code, 0);  /* 报告按键提起事件 */
    input_sync(inputdev);
}

static int __devinit iMX28x_key_init(void)
{
    int i = 0, ret = 0;
    int irq_no = 0;
    int code, gpio;
    inputdev = input_allocate_device(); /* 为输入设备驱动对象申请内存空间*/
    if (!inputdev) {
    return -ENOMEM;
    }
    inputdev->name = DEVICE_NAME;
    set_bit(EV_KEY, inputdev->evbit); /* 设置输入设备支持按键事件 */
        for (i = 0; i < sizeof(keys_list)/sizeof(keys_list[0]); i++) {
        code = keys_list[i].key_code;
        gpio = keys_list[i].gpio;
        /* 为每个按键都初始化工作队列  */
        INIT_WORK(&(keys_list[i].work), imx28x_scankeypad);
        set_bit(code, inputdev->keybit); /* 设置输入设备支持的键值  */
        /* 为每个按键都初始化 GPIO */
        gpio_free(gpio);
        ret = gpio_request(gpio, "key_gpio");
            if (ret) {
            printk("request gpio failed %d \n", gpio);
            return -EBUSY;
            }
        /* 当 GPIO 被设置为输入工作状态后,就可以检测中断信号 */
        gpio_direction_input(gpio);
        /* 把每个 GPIO 中断响应方式都设置为下降沿响应 */
        irq_no = gpio_to_irq(gpio);
        set_irq_type(gpio, IRQF_TRIGGER_FALLING);
        /* 为每个按键的中断都安装中断处理函数,其私有数据为按键信息在 keys_list 数组下的索引 */
        ret = request_irq(irq_no, imx28x_key_intnerrupt, IRQF_DISABLED, "imx28x_key", (void *)i);
            if (ret) {
            printk("request irq faile %d!\n", irq_no);
            return -EBUSY;
            }
        }
    input_register_device(inputdev); /* 注册设备驱动  */
    printk("EasyARM-i.MX28x key driver up \n");
    return 0;
}

static void __exit iMX28x_key_exit(void)
{
    int i = 0;
    int irq_no;
    for (i = 0; i < sizeof(keys_list)/sizeof(keys_list[0]); i++) {
    irq_no = gpio_to_irq(keys_list[i].gpio); /* 为每个按键释放 GPIO */
    free_irq(irq_no, (void *)i); /* 为每个按键卸载中断处理函数 */
    }
    input_unregister_device(inputdev); /* 注销输入设备驱动 */
    printk(" key driver remove \n");
}

module_init(iMX28x_key_init);
module_exit(iMX28x_key_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("YangYue");

内核定时器实现,用于没有触发信号的输入事件

例如没有触发的触摸屏事件。

动态 timer 由内核自身使用,其实也是其他 Timer 的实现基础。使用动态 Timer 的接口函数有三个:

add_timer()
del_timer()
init_timer()

使用时,先调用 init_timer() 初始化一个定时器,指定到期时间和到期处理函数;初始化完成后,内核代码可以用 add_timer() 启动定时器,或者用 del_timer() 来取消一个已经启动的定时器。
add_timer 采用时间轮算法将定时器加入 per CUP 变量 tvec_bases 中,根据其 expire 时间,可能被加入 5 个 Timer Vector 之一。此后,tick 中断将根据时间轮算法处理。当本 timer 到期时,触发其处理函数。
动态 Timer 有两个方面的用途:一是内核自己使用,比如某些驱动程序需要定时服务的时候使用它;二是用来实现用户层 Timer。下面首先讲解间隔 Timer。

比如要实现一个100ms的中断
可以这样设置:
ps2timer.expires=jiffies +HZ100/1000; mod_timer(&ps2timer,jiffies+ HZ100/1000 );

PS2驱动开发

硬件原理

在这里插入图片描述


DI/DAT:信号流向,从手柄到主机,此信号是一个 8bit 的串行数据,同步传送于时钟
的下降沿。信号的读取在时钟由高到低的变化过程中完成。
DO/CMD:信号流向,从主机到手柄,此信号和 DI 相对,信号是一个 8bit 的串行数据,
同步传送于时钟的下降沿。
NC:空端口;
GND:电源地;
VDD:接收器工作电源,电源范围 3~5V;
CS/SEL:用于提供手柄触发信号。在通讯期间,处于低电平;
CLK:时钟信号,由主机发出,用于保持数据同步;
NC:空端口;
ACK:从手柄到主机的应答信号。此信号在每个 8bits 数据发送的最后一个周期变低并
且 CS 一直保持低电平,如果 CS 信号不变低,约 60 微秒 PS 主机会试另一个外设。在编程
时未使用 ACK 端口。

在这里插入图片描述

软件实现

/*
 * @Author: your name
 * @Date: 2021-04-11 18:25:16
 * @LastEditTime: 2021-04-11 23:46:33
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: /DriverDefine/PS2/PS2.c
 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/gpio.h>
#include <linux/input.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/timer.h>

#define DEVICE_NAME  "joystick"

#define PSB_SELECT      1
#define PSB_L3          2
#define PSB_R3          3
#define PSB_START       4
#define PSB_PAD_UP      5
#define PSB_PAD_RIGHT   6
#define PSB_PAD_DOWN    7
#define PSB_PAD_LEFT    8
#define PSB_L2          9
#define PSB_R2          10
#define PSB_L1          11
#define PSB_R1          12
#define PSB_GREEN       13
#define PSB_RED         14
#define PSB_BLUE        15
#define PSB_PINK        16
#define PSB_TRIANGLE    13
#define PSB_CIRCLE      14
#define PSB_CROSS       15
#define PSB_SQUARE      16

#define PSS_RX          5           
#define PSS_RY          6
#define PSS_LX          7
#define PSS_LY          8

static struct timer_list ps2timer; 
u16 Handkey;
u8 Comd[2]={0x01,0x42};    
u8 Data[9]={0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; //Êý¾Ý´æ´¢Êý×é
u16 MASK[]={
    PSB_SELECT,
    PSB_L3,
    PSB_R3 ,
    PSB_START,
    PSB_PAD_UP,
    PSB_PAD_RIGHT,
    PSB_PAD_DOWN,
    PSB_PAD_LEFT,
    PSB_L2,
    PSB_R2,
    PSB_L1,
    PSB_R1 ,
    PSB_GREEN,
    PSB_RED,
    PSB_BLUE,
    PSB_PINK
};    

typedef struct  
{
    int data_pin;
    int cmd_pin;
    int cs_pin;
    int clk_pin;
}ps2_handler;

ps2_handler ps_define={
    2*32+5,2*32+7,2*32+13,2*32+15,
};

#define DI      gpio_get_value(ps_define.data_pin)
#define DO_H    gpio_direction_output(ps_define.cmd_pin,1)
#define DO_L    gpio_direction_output(ps_define.cmd_pin,0)

#define CS_H    gpio_direction_output(ps_define.cs_pin,1)    
#define CS_L    gpio_direction_output(ps_define.cs_pin,0)    

#define CLK_H   gpio_direction_output(ps_define.clk_pin,1)     
#define CLK_L   gpio_direction_output(ps_define.clk_pin,0)   


typedef struct 
{
   int PHYKEY;
   int LOGICKEY;
}KEY_MAP;
KEY_MAP key_map[16]={
    {PSB_SELECT,    KEY_1},
    {PSB_L3,        KEY_A},
    {PSB_R3,        KEY_B},
    {PSB_START,     KEY_C},
    {PSB_PAD_UP,    KEY_A},
    {PSB_PAD_RIGHT, KEY_A},
    {PSB_PAD_DOWN,  KEY_A},
    {PSB_PAD_LEFT,  KEY_A},
    {PSB_L2,        KEY_1},
    {PSB_R2,        KEY_2},
    {PSB_L1,        KEY_3},
    {PSB_R1,        KEY_4},
    {PSB_GREEN,     KEY_A},
    {PSB_RED,       KEY_ENTER},
    {PSB_BLUE,      KEY_7},
    {PSB_PINK,      KEY_8},
};

struct input_dev *inputdev;
void PS2_Cmd(u8 CMD)
{
    volatile u16 ref=0x01;
    Data[1] = 0;
    for(ref=0x01;ref<0x0100;ref<<=1)
    {
        if(ref&CMD)
        {
            DO_H;               
        }
        else DO_L;
        CLK_H;                     
        udelay(50);
        CLK_L;
        udelay(50);
        CLK_H;
        if(DI)
            Data[1] = ref|Data[1];
    }
}
void PS2_ReadData(void)
{
    volatile u8 byte=0;
    volatile u16 ref=0x01;
    CS_L;
    PS2_Cmd(Comd[0]); 
    PS2_Cmd(Comd[1]);  

    for(byte=2;byte<9;byte++)        
    {
        for(ref=0x01;ref<0x100;ref<<=1)
        {
            CLK_H;
            CLK_L;
            udelay(50);
            CLK_H;
              if(DI)
              Data[byte] = ref|Data[byte];
        }
        udelay(50);
    }
    CS_H;   
}


void PS2_ClearData()
{
    u8 a;
    for(a=0;a<9;a++)
        Data[a]=0x00;
}
u8 PS2_DataKey()
{
    u8 index;
    PS2_ClearData();
    PS2_ReadData();
    Handkey=(Data[4]<<8)|Data[3];    
    for(index=0;index<16;index++)
    {       
        if((Handkey&(1<<(MASK[index]-1)))==0)
        return index+1;
    }
    return 0;      
}
u8 PS2_AnologData(u8 button)
{
    return Data[button];
}
void PS2_ShortPoll(void)
{
    CS_L;
    udelay(16);
    PS2_Cmd(0x01);
    PS2_Cmd(0x42);
    PS2_Cmd(0X00);
    PS2_Cmd(0x00);
    PS2_Cmd(0x00);
    CS_H;
    udelay(16);
}

void PS2_EnterConfing(void)
{
    CS_L;
    udelay(16);
    PS2_Cmd(0x01);
    PS2_Cmd(0x43);
    PS2_Cmd(0X00);
    PS2_Cmd(0x01);
    PS2_Cmd(0x00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    CS_H;
    udelay(16);
}

void PS2_TurnOnAnalogMode(void)
{
    CS_L;
    PS2_Cmd(0x01);
    PS2_Cmd(0x44);
    PS2_Cmd(0X00);
    PS2_Cmd(0x01);
    PS2_Cmd(0xEE); 
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    CS_H;
    udelay(16);
}

void PS2_VibrationMode(void)
{
    CS_L;
    udelay(16);
    PS2_Cmd(0x01);
    PS2_Cmd(0x4D);
    PS2_Cmd(0X00);
    PS2_Cmd(0x00);
    PS2_Cmd(0X01);
    CS_H;
    udelay(16);
}

void PS2_ExitConfing(void)
{
    CS_L;
    udelay(16);
    PS2_Cmd(0x01);
    PS2_Cmd(0x43);
    PS2_Cmd(0X00);
    PS2_Cmd(0x00);
    PS2_Cmd(0x5A);
    PS2_Cmd(0x5A);
    PS2_Cmd(0x5A);
    PS2_Cmd(0x5A);
    PS2_Cmd(0x5A);
    CS_H;
    udelay(16);
}

void PS2_SetInit(void)
{
    PS2_ShortPoll();
    PS2_ShortPoll();
    PS2_ShortPoll();
    PS2_EnterConfing(); 
    PS2_TurnOnAnalogMode(); 
    PS2_VibrationMode(); 
    PS2_ExitConfing();
}

void PS2_Vibration(u8 motor1, u8 motor2)
{
    CS_L;
    udelay(16);
    PS2_Cmd(0x01); 
    PS2_Cmd(0x42); 
    PS2_Cmd(0X00);
    PS2_Cmd(motor1);
    PS2_Cmd(motor2);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    PS2_Cmd(0X00);
    CS_H;
    udelay(16);
}


void ps2_timer_function(unsigned long data)
{
    u8 val;
    val=PS2_DataKey();
    printk("%d \t\n",val);
    if(val==0)
    {
        input_event(inputdev,EV_KEY,key_map[val].LOGICKEY, 0);  //上报EV_KEY类型,button按键,0(没按下)
        input_sync(inputdev);
    }else
    {
        input_event(inputdev,EV_KEY,key_map[val].LOGICKEY, 1);  //上报EV_KEY类型,button按键,0(没按下)
        input_event(inputdev,EV_REL,PS2_AnologData(), 1); 
        input_sync(inputdev);        
        //printk("%d \t\n",key_map[val].LOGICKEY);
    }
    mod_timer(&ps2timer,jiffies+ HZ*100/1000  );
}

static int PS2_init(void)
{
    int i = 0;
    inputdev = input_allocate_device(); /* 为输入设备驱动对象申请内存空间*/
    if (!inputdev) {return -ENOMEM;}
    inputdev->name = DEVICE_NAME;

    set_bit(EV_KEY, inputdev->evbit); /* 设置输入设备支持按键事件 */
    set_bit(EV_REL, inputdev->evbit); 

    set_bit(KEY_A,inputdev->keybit);                  //支持按键 L
    set_bit(KEY_B,inputdev->keybit);                //支持按键 S
    set_bit(KEY_C,inputdev->keybit);      //支持按键 空格
    set_bit(KEY_ENTER,inputdev->keybit);


    //GPIO
    gpio_request(ps_define.data_pin, "data");
    gpio_direction_input(ps_define.data_pin);
    gpio_request(ps_define.cs_pin, "cs");
    gpio_direction_output(ps_define.cs_pin,0); 
    gpio_request(ps_define.clk_pin, "clk");
    gpio_direction_output(ps_define.clk_pin,0);
    gpio_request(ps_define.cmd_pin, "cmd");
    gpio_direction_output(ps_define.cmd_pin,0);
    PS2_SetInit();
    input_register_device(inputdev); /* 注册设备驱动  */

    init_timer(&ps2timer);
    ps2timer.function=ps2_timer_function;
    ps2timer.expires=jiffies +HZ*100/1000;
    add_timer(&ps2timer);

    printk("YURI PS2 driver up \n");
    return 0;
}

static void PS2_exit(void)
{
    del_timer(&ps2timer);
    input_unregister_device(inputdev); /* 注销输入设备驱动 */
    input_free_device(inputdev);
    printk("PS2 driver remove \n");
}

module_init(PS2_init);
module_exit(PS2_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("YURI YANG");

【Linux系统编程】Linux系统编程学习笔记

@[toc]

1. (一)系统参数以及选项

1.1. getopt()


函数的出处就是unistd.h头文件(哈哈),写代码的时候千万不要忘记把他老人家include上。

原型:
int getopt(int argc,char * const argv[ ],const char * optstring);
前两个参数大家不会陌生,没错,就是老大main函数的两个参数!老大传进来的参数自然要有人接着!
第三个参数是个字符串,看名字,我们可以叫他选项字符串(后面会说明)
返回值为int类型,我们都知道char类型是可以转换成int类型的,每个字符都有他所对应的整型值,其实这个返回值返回的就是一个字符,什么字符呢,叫选项字符(姑且这么叫吧,后面会进一步说明)

简单了解了出身和原型,下面我们看看这家伙到底有什么本事吧!

小弟1、extern char* optarg;
小弟2、extern int optind;
小弟3、extern int opterr;
小弟4、extern int optopt;

队形排的不错。小弟1是用来保存选项的参数的(先混个脸熟,后面有例子);小弟2用来记录下一个检索位置;小弟3表示的是是否将错误信息输出到stderr,为0时表示不输出,小弟4表示不在选项字符串optstring中的选项(有点乱哈,后面会有例子)

开始逐渐解释上面遗留的问题。

问题1:选项到底是个什么鬼?

在linux下大家都用过这样一条指令吧:gcc helloworld.c -o helloworld.out; 这条指令中的-o就是命令行的选项,而后面的helloworld.out就是-o选项所携带的参数。当然熟悉shell指令的人都知道(虽然我并不熟悉),有些选项是不用带参数的,而这样不带参数的选项可以写在一起(这一点在后面的例子中会用到,希望理解),比如说有两个选项-c和-d,这两个选项都不带参数(而且明显是好基友),那么他们是可以写在一起,写成-cd的。实际的例子:当我们删除一个文件夹时可以使用指令 rm 目录名 -rf,本来-r表示递归删除,就是删除文件夹中所有的东西,-f表示不提示就立刻删除,他们两个都不带参数,这时他们就可以写在一起。

问题2:选项字符串又是何方神圣?

还是看个例子吧

“a:b:cd::e”,这就是一个选项字符串。对应到命令行就是-a ,-b ,-c ,-d, -e 。冒号又是什么呢? 冒号表示参数,一个冒号就表示这个选项后面必须带有参数(没有带参数会报错哦),但是这个参数可以和选项连在一起写,也可以用空格隔开,比如-a123 和-a 123(中间有空格) 都表示123是-a的参数;两个冒号的就表示这个选项的参数是可选的,即可以有参数,也可以没有参数,但要注意有参数时,参数与选项之间不能有空格(有空格会报错的哦),这一点和一个冒号时是有区别的。

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-04-10 17:11:40
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-04-10 17:31:19
 */
#include <unistd.h>
#include <stdio.h>

int main(int argc,char** argv)
{
    int oc;
    while ((oc=getopt(argc,argv,":a::b:c:"))!=-1)
    {
        printf("optind %d opterr %d \n",optind,opterr);
        switch (oc)
        {
        case 'a':
            printf("option a %s\n",optarg);
            break;
        case 'b':
            printf("option b %s\n",optarg);
            break;
        case 'c':
            printf("option c %s\n",optarg);
            break;
        case '?':
            printf("UNKNOWN %c\n",optopt);
            break;
        case ':':
            printf("LESS OPT %c\n",optopt);
            break;
        default:
            break;
        }
    }
    return 0;
}

1.2. getopt_long()

支持长选项的方法
头文件
原型:
int getopt_long(int argc, char * const argv[], const char *optstring, const struct option *longopts, int *longindex)

struct option
{
const char *name;
int has_arg;
int *flag;
int val;
};

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-04-10 17:36:45
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-04-10 17:36:45
 */
#include <stdio.h>     /* for printf */
#include <stdlib.h>    /* for exit */
#include <getopt.h>

int
main(int argc, char **argv)
{
    int c;
    int digit_optind = 0;

   while (1) {
        int this_option_optind = optind ? optind : 1;
        int option_index = 0;
        static struct option long_options[] = {
            {"add",     required_argument, 0,  0 },
            {"append",  no_argument,       0,  0 },
            {"delete",  required_argument, 0,  0 },
            {"verbose", no_argument,       0,  0 },
            {"create",  required_argument, 0, 'c'},
            {"file",    required_argument, 0,  0 },
            {0,         0,                 0,  0 }
        };

       c = getopt_long(argc, argv, "abc:d:012",
                 long_options, &option_index);
        if (c == -1)
            break;

       switch (c) {
        case 0:
            printf("option %s", long_options[option_index].name);
            if (optarg)
                printf(" with arg %s", optarg);
            printf("\n");
            break;

       case '0':
        case '1':
        case '2':
            if (digit_optind != 0 && digit_optind != this_option_optind)
              printf("digits occur in two different argv-elements.\n");
            digit_optind = this_option_optind;
            printf("option %c\n", c);
            break;

       case 'a':
            printf("option a\n");
            break;

       case 'b':
            printf("option b\n");
            break;

       case 'c':
            printf("option c with value '%s'\n", optarg);
            break;

       case 'd':
            printf("option d with value '%s'\n", optarg);
            break;

       case '?':
            break;

       default:
            printf("?? getopt returned character code 0%o ??\n", c);
        }
    }

   if (optind < argc) {
        printf("non-option ARGV-elements: ");
        while (optind < argc)
            printf("%s ", argv[optind++]);
        printf("\n");
    }

   exit(EXIT_SUCCESS);
}

2. (二)环境管理

2.1. extern char **environ;全局变量

Linux环境变量的所有值都存放在这个全局变量里面
举例说明:打印出所有的环境变量  
```c
    #include <stdio.h>
    #include <stdlib.h>
    extern char **environ;  //导入这个全局变量
    int main(int argc, char* agrv[]){
            int i;
            for(i=0;environ[i];i++){
                       printf("%s\n",environ[i]);
            }
            return 0;   
    }
```

2.2. getenv获取环境变量的值

char *getenv(const char *name); //name名称,环境变量是键值对的形式,根据名称获取其值
返回值:成功返回对应的值,失败返回NULL 。

2.3. setenv设置环境变量的值

int setenv(const char *name, const char *value, int overwrite); //设置,name键,value值,overwrite如果已经存在是否覆盖
返回值:成功返回0;失败返回-1 。  
int unsetenv(const char *name); //清除指定名称的环境变量的值
返回值:成功返回0;失败返回-1 。 
int clearenv(void); //清除所有的环境变量
返回值成功返回0;失败返回非0 。 
 int putenv(char *string);   //改变或添加一个环境变量,string参数的形式name=value
 返回值成功返回0;失败返回非0 。

3. (三)内存管理

3.1. 基础理论

代码段: 只读,可共享; 代码段(code segment/text segment )通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

数据段: 储存已被初始化了的静态数据。数据段(data segment )通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。

BSS 段:未初始化的数据段. BSS 段(bss segment )通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS 是英文Block Started by Symbol 的简称。BSS 段属于静态内存分配。

[ 注意:BSS段 和 data段的区别是 ,如果一个全局变量没有被初始化(或被初始化为0),那么他就存放在bss段;如果一个全局变量被初始化为非0,那么他就被存放在data段。]

堆(heap ): 堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)

栈(stack) :栈又称堆栈,是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{} ”中定义的变量(但不包括static 声明的变量,static 意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/ 恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
程序空间

注意:
图中间在栈和堆之间,有一个共享内存的映射的区域。这个就是共享内存存放的地方。一般共享内存的默认大小是32M。

1、经过初始化的全局变量和静态变量保存在数据段中。

2、未经初始化的全局变量和静态变量保存在BSS段。

3、函数内部声明的局部变量保存在堆栈段中。

4、const修饰的全局变量保存在文本段中,const修饰的局部变量保存在堆栈段中。

5、字符串常量保存在文本段中

3.2. 常见类型

size_t
代表内存大小,大多系统中相当于unsigned long类型
NULL
ssize_t
有符号长度,可以返回负值

3.3. 内存分配库函数

3.3.1. malloc

void malloc(unsigned int num_bytes); 动态内存分配,用于申请一块连续的指定大小的内存块区域以void类型返回分配的内存区域地址,即再分配内存之后,该指针已经保存了一个地址。
void* 类型表示未确定类型的指针,一般需要强制转换为需要的指针类型。

3.3.2. calloc

申请空间并清零

3.3.3. realloc

void *realloc(void *mem_address, unsigned int newsize);
先判断当前的指针是否有足够的连续空间,如果有,扩大mem_address指向的地址,并且将mem_address返回,如果空间不够,先按照newsize指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem_address所指内存区域(注意:原来指针是自动释放,不需要使用free),同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。

3.3.4. free

void free(void *ptr);
释放malloc(或calloc、realloc)函数给指针变量分配的内存空间的函数.
注意:在free指针ptr指向的内存之后,为了避免之后重新用free错误释放这块儿已经释放的内存,一般需要将ptr=NULL;

1.访问已经释放的指针 有可能出现段错误
2.两次释放相同的指针
3.释放内存失败 要特别注意在循环递归中的内存申请,如果不停申请将导致最后没有内存申请而死机

3.3.5. memset

void *memset(void *s, int ch, size_t n);
将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法

3.3.6. memcpy

void *memcpy(void *dest, const void *src, size_t n);
从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中。
这里需要对memcpy这个函数做一些说明,memcpy函数后经常还会有一个malloc函数,那么这里就有一个疑问,为什么用memcpy函数拷贝了一份内存之后还要使用malloc函数,下面我们就来学习一下。

3.3.7. 更接近底层的函数

int brk(void *addr);
void *sbrk(intptr_t increment);

3.3.8. 内存分配原理

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

1、brk是将数据段(.data)的最高地址指针_edata往高地址推;

2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

 这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的

(I)、malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图

1、进程启动的时候,其(虚拟)内存空间的初始布局如图1所示。
其中,mmap内存映射文件是在堆和栈的中间(例如libc-2.2.93.so,其它数据文件等),为了简单起见, 省略了内存映射文件。
_edata指针(glibc里面定义)指向数据段的最高地址。
2、进程调用A=malloc(30K)以后,内存空间如图2:
malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。
你可能会问: 只要把_edata+30K就完成内存分配了?
事实是这样的,_edata+30K只是完成虚拟地址的分配, A这块内存现在还是没有物理页与之对应的, 等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。 也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。
3、进程调用B=malloc(40K)以后,内存空间如图3。
(II)malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),如下图:

4、进程调用C=malloc(200K)以后,内存空间如图4:
默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。
这样子做主要是因为::
brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放。
当然,还有其它的好处,也有坏处,再具体下去,有兴趣的同学可以去看glibc里面malloc的代码了。
5、进程调用D=malloc(100K)以后,内存空间如图5;
6、进程调用free(C)以后,C对应的虚拟内存和物理内存一起释放。

7、进程调用free(B)以后,如图7所示:
B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?
当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了。
8、进程调用free(D)以后,如图8所示:
B和D连接起来,变成一块140K的空闲内存。
9、默认情况下:
当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示。

4. C语言错误 errno

Linux中系统调用的错误都存储于 errno中,errno由操作系统维护,存储就近发生的错误,即下一次的错误码会覆盖掉上一次的错误。

PS: 只有当系统调用或者调用lib函数时出错,才会置位errno!
查看系统中所有的errno所代表的含义,可以采用如下的代码:

#include <stdio.h>
#include <string.h>  
#include <errno.h>
int main()
{
    int tmp = 0;
    for(tmp = 0; tmp <=256; tmp++)
    {
        printf("errno: %2d\t%s\n",tmp,strerror(tmp);
    }
    return 0;
}

常见用法:

(1)void perror(const char *s)

函数说明
perror ( )用来将上一个函数发生错误的原因输出到标准错误(stderr),参数s 所指的字符串会先打印出,后面再加上错误原因 字符串。此错误原因依照全局变量 errno 的值来决定要输出的字符串。

(2) char *strerror(int errno)

将错误代码转换为字符串错误信息,可以将该字符串和其它的信息组合输出到用户界面例如
fprintf(stderr,“error in CreateProcess %s, Process ID %d “,strerror(errno),processID)
注:假设processID是一个已经获取了的整形ID
(3)printf(”%m”, errno);

另外不是所有的地方发生错误的时候都可以通过error获取错误代码,例如下面的代码段

5. Linux文件IO

Linux中,一切皆文件。文件为操作系统服务和设备提供了一个简单而一致的接口。这意味着程序完全可以像使用文件那样使用磁盘文件、串行口、打印机和其他设备。
也就是说,大多数情况下,你只需要使用5个函数: open、close、read、write和ioctl。 例外的情况: 目录的读写,网络连接等特殊文件

5.1. 目录

文件通常由两部分组成: 内容 + 属性,即管理信息,包括文件的创建修改日期和访问权限等。属性均保存在 inode 节点中。inode – “索引节点”,储存文件的元信息,比如
文件的创建者、文件的创建日期、文件的长度和文件在磁盘上存放的位置等等。每个inode都有一个号码,操作系统用inode号码来识别不同的文件。ls -i 查看inode 号。

目录是用于保存其他文件的节点号和名字的文件,每个数据项为指向文件节点的链接。如下图:

当文件链接数变为零,意味文件删除,磁盘空间变成可用空间。

5.2. 文件和设备

三个重要的设备文件:
/dev/console – 系统控制台。
/dev/tty – 访问不同的物理设备。
/dev/null – 空设备,向所有写这个设备的输出都将被丢弃。

设备驱动程序:
操作系统的核心部分,即内核,是由一组设备驱动程序组成。他们是一组对系统硬件进行控制的底层接口,为了向用户提供一个一致的接口,其封装了所有与硬件相关的特性。
硬件特有功能可通过ioctl(用于I/O控制)系统调用来提供。
/dev 目录下的设备文件都可以被打开、读、写和关闭。
1)open : 打开文件或设备。
2)read : 从打开的文件或设备里读数据。
3)write: 向文件或设备写数据。
4)close: 关闭文件或设备。
5)ioctl: 把控制信息传递给设备驱动程序,每个驱动都由自己的一组 ioctl 命令。

库函数
针对输入输出操作直接使用底层系统调用效率非常低,原因由如下两点。
1)使用系统调用会影响系统性能。
2)硬件会对底层系统调用一次所读写的数据块大小做限制。磁盘:至少一个扇区512
字节,磁带,一次 10K
库函数给设备和磁盘文件提供了更高层的接口,即标准函数库。使用它你可以高效读写任意长度的数据块,库函数则在数据满足条件后再安排系统调用。这样极大降低了开销。
注:库函数的文档一般放在手册的第三页,每个库函数有其对应的头文件。

底层文件访问
运行中的程序称为进程,每个进程都有与之关联的文件描述符。
文件描述符 - 一些小值整数,通过他们访问打开的文件或设备。开始运行会有三个文件描述符:
1)0: 标准输入 STDIN_FILENO
2)1: 标准输出 STDOUT_FILENO
3)2: 标准错误 STDERR_FILENO
文件描述符的变化范围是:0~OPEN_MAX-1 (可通过ulmit -a 查看)

5.3. write系统调用

作用:把缓冲区buf 的前count 个字节写入与文件描述符 fd 相关联的文件中。

size_t write(int fd,const void *buf, size_t count);

描述符出错,文件达到进程限制最大值或设备驱动程序对数据块 长度比较敏感,该返回值可能会小于count,这并不是一个错误。 0 表示未写入数据; -1 表示出错,错误代号在全局变量 errno里。
复制代码
“`C

include

include

include

include

int main()
{
const char * output = “Hello World\n”;
const char * errstr = “A Write error has occurred on file descriptior !\n”;
if(write(1,output,strlen(output))!=strlen(output));
write(2,errstr,strlen(errstr));
exit(0);
}

## 5.4. read系统调用

作用:作用:从与文件描述符 fd 相关联的文件中读取前count 个字节到缓冲区buf 中。
<unistd.h>
size_t write(int fd,const void *buf, size_t count);

它返回实际读入的字节数,这可能会小于请求的字节数。 0 表示未读入任何数据,已到达了文件尾部。 -1 表示出错,错误代号在全局变量 errno里。

C

include

include

int main()
{
char buffer[128];
int nread = read(0,buffer,128);
if(nread == -1)
write(2,”A read error has occurred\n”,26);
if(write(1,buffer,nread)!= nread)
write(2,”A write error has occurred\n”,27);

}

## 5.5. open系统调用

作用:创建一个新的文件描述符(文件或设备)。

C

include

include

include

int open(const char *pathname, int flags, mode_t mode);

open 建立一条到文件或设备的访问路径。成功后可获得供 read、write和其他系统调用使用的唯一的文件描述符。此文件描述符进程唯一;如果两个程序打开同一个文件,那么,他们分别得到不同的文件描述符,并可以单独对文件进行独立的操作。我们可以通过文件锁(O_EXCL或FCNTL)功能来解决这个问题。

参数说明:
pathname - 指示准备打开的文件或设备的名字;      
flags    - 用于指定打开文件所采取的动作;            
mode    - 用于指定创建文件的权限,O_CREATE 才使用。     
flags 参数通过必需文件访问模式 与 其他可选模式相结合的方式来指定。 首先必须指定如下文件访问模式之一:       
模     式                   
O_RDONLY    以只读方式打开      
O_WRONLY    以只写方式打开      
O_RDWR      以读写方式打开      
可选模式组合:      
1) O_APPEND: 把写入数据最佳在文件的末尾。      
2) O_TRUNC:  打开文件时把文件长度设置为零,丢弃已有的内容。        
3) O_CREAT:   如果需要,就按参数mode 中给出的访问模式创建文件。        
4) O_EXCL:   与O_CREAT一起使用,确保创建文件的 原子操作。如果文件存在,创建 将失败     


访问权限的初始:    
单个权限设置     : 
S_I  R或W或X    USR或GRP或OTH       
读写执行全权限 : 
S_I RWX             U或G或O       
如: S_IRUSR    读权限  文件属性        
   S_IRWXO   读写执行  其他用户       
最终权限生成还和进程设置的umask权限掩码有关,执行umask命令或者函数 可以改变权限。   
新文件描述符总使用未用文件描述符的最小值。如果一个文件符被关闭再次调用open ,其马上会被重用。
 !Posix 规定了一个 creat 调用:  等同于  O_CREAT|O_WRONLY|O_TRUNC 

## 5.6. close系统调用
作用:终止文件描述符fd 和对应文件(文件或设备)的关联。文件描述符被释放并能够重新使用。close 调用成功返回0,出错返回 -1。

c

include

int close(int fd);

返回值: 检查 close 调用的返回值很重要。可以检测某些写操作错误!

## 5.7. ioctl系统调用

ioctl提供了一个用于控制设备及其描述行为和配置底层的服务的接口。终端文件描述符、套接字都可以定义他们的ioctl,具体需要参考特定设备的手册。

c

include

int ioctl(int d, int request, …);

## 5.8. dup和dup2的系统调用
作用:提供了一种复制文件描述符的方法,是我们通过两个或者更多不同的描述符来访问同一个文件,主要用于多个进程间进行管道通信。

c

include

   int dup(int oldfd);
   int dup2(int oldfd, int newfd);
## 5.9. lseek系统调用

作用:
作用:lseek 对文件描述符 fd 的读写指针进行设置。也就是说,设置文件的下一个读写位置。可根据绝对位置和相对位置(当前位置或文件尾部)进行设置。

c

include

include

off_t lseek(int fd, off_t offset, int whence);

offset 参数用来指定位置,而whence 参数定义该偏移值的用法。Whence 可取值如下:


1)SEEK_SET:     offset 是一个绝对位置。        
2)SEEK_CUR:    offset 是相对于当前位置的一个相对位置。     
3)SEEK_END:    offset 是相对于文件尾的一个相对位置。       
 lseek 返回从文件头到文件指针被设置处的字节偏移值,失败时返回-1.        



## 5.10. fstat、stat和lstat系统调用

作用:获取文件的状态信息,该信息将会写入一个buf中,buf的地址会以参数的形式传递给fstat

    ```c
       #include <sys/types.h>
       #include <sys/stat.h>
       #include <unistd.h>

       int stat(const char *path, struct stat *buf);

       int fstat(int fd, struct stat *buf);

       int lstat(const char *path, struct stat *buf);
    ```

stat 和 lstat 均通过文件名查询状态信息,当文件名是符号链接时,lstat返回的时符号链接本身的信息,而stat 返回的时改链接指向的文件的信息。

struct stat {                                      
               dev_t     st_dev;     /* ID of device containing file */    
               ino_t     st_ino;     /* inode number */               
               mode_t    st_mode;    /* protection */                
               nlink_t   st_nlink;   /* number of hard links */           
               uid_t     st_uid;     /* user ID of owner */             
               gid_t     st_gid;     /* group ID of owner */            
               dev_t     st_rdev;    /* device ID (if special file) */    
               off_t     st_size;    /* total size, in bytes */           
               blksize_t st_blksize; /* blocksize for filesystem I/O */      
               blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
               time_t    st_atime;   /* time of last access */           
               time_t    st_mtime;   /* time of last modification */     
               time_t    st_ctime;   /* time of last status change */     
           }; 
这里要特别提到的是,以上 st_mode 标志有一系列相关的宏,定义见 sys/stat.h 中
,可用来测试文件类型,如:
![](https://img-blog.csdnimg.cn/img_convert/f335997f593c1043c03099b1758fad54.png)


错误处理

许多系统调用和函数都会因为各种各样的原因失败。他们失败时设置外部变量errno 来知名失败原因。许多不同函数库都把这个变量用做报告错误的标准方法。
注意: 程序必须在函数报告出错 之后立刻检查errno 变量,因为它可能马上就被下一个函数调用所覆盖,即使下一个函数没有出错,也可能会覆盖这个变量。
常用错误代码的取值和含义如下:
l   EPERM:     操作不允许

l   ENOENT:   文件或目录不存在。

l   EINTR:     系统调用被中断。

l   EAGAIN:    重试,下次有可能成功!

l   EBADF:     文件描述符失效或本身无效

l   EIO:        I/O错误。

l   EBUSY:     设备或资源忙。

l   EEXIST:     文件存在。

l   EINVL:      无效参数。

l   EMFILE:     打开的文件过多。

l   ENODEV:    设备不存在。

l   EISDIR:      是一个目录。

l  ENOTDIR:     不是一个目录。


## 5.11. Linux目录操作

### 5.11.1. 一、chdir 函数

       1、作用:修改当前进程的路径
       2、函数原型:

           #include <unistd.h>
                 int chdir(const char *path);
### 5.11.2. 二、getcwd 函数
       1、作用:获取当前进程工作目录
       2、函数原型:
           #include <unistd.h>
                   char *getcwd(char *buf, size_t size);
                   char *getwd(char *buf);
 注:一般情况下 chdir 函数 和 getcwd 函数配合在一起使用

chdir 函数和 getcwd 函数的运用:

c
#include
#include
#include
#include
#include
#include

int main(int argc, char *argv[] )
{

    if( argc<2 )
    {
        perror("./a.out filepath");
        exit(1);
    }

    printf(" agrv[1] = %s\n",argv[1]);
    // 修改当前的路径
    int ret =chdir(argv[1]);
    if( ret == -1 )
    {
        perror("chdir");
        exit(1);    
    }

    // 在这里通过在改变后的目录下创建一个新的文件,来证明目录已经改变
    int fd = open("chdir.txt",O_CREAT|O_RDWR,0644);
    if( fd == -1 )
    {
        perror("open"); 
        exit(1);
    }

    close(fd);

    // 获取改变目录后的目录名
    char buf[100]={0};

    getcwd(buf,sizeof(buf));
    printf("current dir: %s\n",buf);

    return 0;
}
### 5.11.3. 三、rmdir 函数

       1、作用:删除一个目录
       2、函数原型:
           #include <unistd.h>
                  int rmdir(const char *pathname);
### 5.11.4. 四、mkdir 函数

       1、作用:创建一个目录
       2、函数原型:
           #include <sys/stat.h>
           #include <sys/types.h>
               int mkdir(const char *pathname, mode_t mode);

### 5.11.5. 五、opendir  函数

       1、作用:打开一个目录
       2、函数原型:
           #include <sys/types.h>
           #include <dirent.h>
               DIR *opendir(const char *name);
               DIR *fdopendir(int fd);
       3、返回值:
             (1)、DIR结构体指针,该结构是一个内部结构,保存所打开的目录的信息,作用于类似于FILE结构。
             (2)、函数出错,返回NULL
### 5.11.6. 六、readdir 函数
       1、作用:读目录
       2、函数原型:
           #include <dirent.h>
               struct dirent *readdir(DIR *dirp);
       3、返回值:
             返回一个记录项 
             struct dirent {
                   ino_t          d_ino;       /* inode number */                  // 目录进入点的 inode
                   off_t          d_off;       /* not an offset; see NOTES */      // 目录文件头开始至此目录进入点的位移
                   unsigned short d_reclen;    /* length of this record */         // d_name 长度
                   unsigned char  d_type;      /* type of file; not supported      // d_name 所指的文件夹 
                                                  by all filesystem types */
                   char           d_name[256]; /* filename */                      // 文件名
             };     
             d_tyep 有 8 种类型:
                (1)、 DT_BLK      This is a block device.           块设备
                (2)、 DT_CHR      This is a character device.       字符设备
                (3)、 DT_DIR       This is a directory.              目录
                (4)、 DT_FIFO     This is a named pipe (FIFO).      管道
                (5)、 DT_LNK      This is a symbolic link.          软链接
                (6)、 DT_REG      This is a regular file.           普通文件
                (7)、 DT_SOCK     This is a UNIX domain socket.     套接字
                (8)、 DT_UNKNOWN     The file type is unknown.      未知类型

 ### 七、closedir 函数
        1、作用:关闭一个目录
        2、函数原型:
           #include <sys/types.h>
           #include <dirent.h>

           int closedir(DIR *dirp);
        3、返回值:
            若函数执行成功,返回0;若失败,返回 -1.
opendir、readdir、closedir 三个函数 的综合运用:

c
#include
#include
#include
#include
#include
#include
#include

// 获取 root 目录下的文件个数
int get_file_count(char *root)
{
    DIR * dir = NULL;
    dir = opendir(root);
    if( NULL == dir )
    {
        perror("opendir");
        exit(1);
    }
    // 遍历当前打开的目录
    struct dirent* ptr = NULL;
    char path[1024]={0};
    int total = 0;
    while( (ptr = readdir(dir) )!= NULL)
    {
        // 过滤掉 . 和 ..
        if( strcmp(ptr->d_name,".") == 0 || strcmp(ptr->d_name,"..") == 0 )
        {

            continue;   
        }
        // 如果是目录,递归读目录
        if(ptr->d_type == DT_DIR)
        {
            sprintf(path,"%s/%s",root,ptr->d_name);
            total += get_file_count(path);
        }
        // 如果是普通文件
        if( ptr->d_type == DT_REG )
        {
                total++;    
        }
    }
    // 关闭目录
    closedir(dir);
    return total;
}
int main(int argc,char *argv[])
{
    if( argc<2 )
    {
        perror("./a.out dir\n");
        exit(1);    
    }
    // 获取文件个数
    int count = get_file_count(argv[1]);
    printf("%s has file numbers : %d\n",argv[1],count); 
    return 0;
}
### 5.11.7. 八、dup 和 dup2 函数
       1、作用:复制现有的文件描述符
       2、函数原型:       

             #include <unistd.h>
           int dup(int oldfd);
           int dup2(int oldfd, int newfd);

       3、返回值:
              (1)、dup 返回的是文件描述符中没有被占用的
              (2)、dup2 分两种情况讨论下:
                      (a)、oldfd----->newfd 如果  newfd 是一个被打开的文件描述符,在拷贝前会先关掉 newfd
                      (b)、oldfd------>newfd是同一个文件描述符,不会关掉 newfd , 直接返回 oldfd

c
#include
#include
#include
#include
#include
#include
#include

int main(void)
{
    int fd =open("a.txt",O_RDWR);
    if( fd == -1 )
    {
        perror("open");
        exit(1);
    }

    printf("file open fd = %d\n",fd);

    // 找到进程文件描述符表   ======= 第一个========== 可用的文件描述符
    // 将参数指定的文件复制到该描述后          返回这个描述符

    int ret = dup(fd);
    if( fd == -1 )
    {
        perror("dup");
        exit(1);
    }

    printf(" dup fd = %d\n",ret);
    char *buf = "你是猴子请来的救兵吗??\n";
    char *buf1 = "你大爷的,我是程序猿!!!\n";

    write(fd,buf,strlen(buf));
    write(ret,buf1,strlen(buf1));

    close(fd);

    return 0;
}

#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
    int fd =open("english.txt",O_RDWR);
    if( fd == -1 )
    {
        perror("open");
        exit(1);
    }
    int fd1 =open("a.txt",O_RDWR);
    if( fd1 == -1 ) 
    {
        perror("open");
        exit(1);
    }
    printf("fd = %d\n",fd);
    printf("fd1 = %d\n",fd1);
    int ret = dup2(fd1, fd);
    if( ret == -1 )
    {   
        perror("dup2");
        exit(1);
    }
    printf(" current fd = %d\n",ret);
    char *buf = "主要看气质 !!!!!!!!!!!!!!!!!\n";
    write(fd,buf,strlen(buf));
    write(fd1,"hello world!",12);
    close(fd);
    close(fd1);
    return 0;
}
### 5.11.8. 九、fcntl 函数
         1、作用:改变已经打开文件的属性

         2、函数原型:       

             #include <unistd.h>
             #include <fcntl.h>

           int fcntl(int fd, int cmd, ... /* arg */ );             这是一个可变长参数的函数


       3、功能:
             (1)、复制一个现有的描述符            F_DUPFD
             (2)、 获得 / 设置文件状态标价        cmd( 参数设置如下 )            
                       (a)、F_GETFD
                       (b)、F_STFD
             (3)、获得 / 设置文件标记状态
                       (a)、
                               O_RDONLY                      只读打开
                               O_WRONLY                      只写打开
                               O_RDWR                        读写打开
                               O_EXEC                        执行打开
                               O_SEARCH                      搜索打开
                               O_APPEND                      追加打开
                               O_NONBLOCK                    非阻塞模式
                       (b)、F_SETFL
                               O_APPEND                     
                               O_NONBLOCK               
             (4)、  获得  / 设置异步 I / O 所有权
                       (a)、F_GETOWN
                       (b)、F_SETOWN
             (5)、获得 / 设置记录锁
                       (a)、F_GETLK
                       (b)、F_SETLK
                       (c)、SETLKW

## 5.12. 修改文件权限,所有权

### 5.12.1. 更改用户组

c
#include
int chown(const char *pathname,uid_t owner,gid_t group);
int fchown(int fd,uid_t owner,gid_t group);
int fchownat(int fd,const char *pathname,uid_t owner,gid_t group,int flag);

        这4个函数可用于更改文件的用户ID和组ID,如果参数owner或group中的任意一个是-1,则对应的ID不变。

        除了所引用的文件是符号连接以外,这4个函数的操作类似。在符号链接情况下,lchown和fchownat(设置了AT_SYMLINK_NOFOLLOW标志)更改符号链接本身的所有者,而不是该符号链接所指向的文件的所有者。

        fchown函数与chown或者lchown函数在下面两种情况下是相同的:一种是pathname参数为绝对路径,另一种是fd参数取值为AT_FDCWD而pathname参数为相对路径。在这两种情况下,如果flag参数中设置了AT_SYMLINK_NOFOLLOW标志,fchownat与lchown行为相同,如果flag参数中清除了AT_SYMLINK_NOFOLLOW标志,则fchownat与chown行为相同。如果fd参数设置为打开目录的文件描述符,并且pathname参数是一个相对路径名,fchownat函数计算相对于打开目录的pathname。

### 5.12.2. 更改权限
<sys/stat.h>
函数定义:
//通过传入path中给定的文件名的方式来改变文件制定的权限
int chmod(const char *path,mode_t mode);
//通过传入文件描述符的方式为一个文件重设权限
int fchmod(int fd,mode_t mode);

# 6. 时间管理

 ## 1、时间类型。Linux下常用的时间类型:time_t,struct timeval,struct tm

(1)time_t是一个长整型,一般用来表示用1970年以来的秒数。
(2)Struct timeval有两个成员,一个是秒,一个是微妙。
    struct timeval
    {
        long tv_sec;
        long tv_usec;
    };
(3)struct tm是直观意义上的时间表示方法:
    struct tm {
        int tm_sec;
        int tm_min;
        int tm_hour;
        int tm_mday;
        int tm_mon;
        int tm_year;
        int tm_wday;
        int tm_yday;
        int tm_isdst;
    };


## 6.1. 时间操作

### 6.1.1. (1) 时间格式间的转换函数
主要是 time_t、struct tm、时间的字符串格式之间的转换。看下面的函数参数类型以及返回值类型
char *asctime(const struct tm *tm);
char *ctime(const time_t *timep);
struct tm *gmtime(const time_t *timep);  //返回的是格林威治时间
struct tm *localtime(const time_t *timep);  //返回的是本地时间
time_t mktime(struct tm *tm);
### 6.1.2. (2) 获取时间函数
两个函数,获取的时间类型看原型就知道了:
time_t time(time_t *t);
int gettimeofday(struct timeval *tv, struct timezone *tz);
前者获取time_t类型,后者获取struct timeval类型,因为类型的缘故,前者只能精确到秒,后者可以精确到微秒。相对于gettimeofday,还有settimeofday用来设置时间


# 7. 通用库接口

## 7.1. (1)assert断言
    <assert.h>

    assert(expression);

    如果为假,就会输出诊断信息,同时退出程序(abort)

## 7.2. (2)自杀函数
    <stdlib.h>

    void abort(void)

## 7.3. 正则表达式匹配框架



标准的正则表达式匹配框架:
    编译正则表达式.
    匹配正则表达式.
    释放正则表达式.

编译正则表达式
 <regex.h>
int Regcomp(regex_t* preg, const char* regex, int cflags);
参数说明
    preg : 用来保存编译之后的结果
    regex : 正则表达式字符串,表示被编译的正则表达式。
    cflags : 编译控制参数
        REG_EXTENDED : 使用扩展正则表达式模式
        REG_ICASE : 对规则中字符串不区分大小写
        REG_NOSUB : 只检查是否有符合规则的子串。

返回值
编译成功返回0,否则返回非0
匹配正则表达式
typedef struct {
  regoff_t rm_so;
  regoff_t rm_eo;
} regmatch_t;
int regexec(const regex_t* preg,
            const char* string,
            size_t nmatch,
            regmatch_t pmatch[],
            int eflags);


参数说明
    preg : 上述编译之后的正则表达式regex_t指针。
    string : 被匹配的字符串。
    match : 被匹配的个数。告诉函数regexec最多可以把多少个匹配结果写入pmatch,一般为pmatch数组的长度。
    pmatch : 匹配结果数组。
    rm_so : 满足子串在string中的起始偏移量
    rm_eo : 满足子串在string中的结束偏移量
    eflags : 匹配的特性
        REG_NOTBOL : 是否为第一行
        REG_NOTEOL : 是否是最后一行

返回值

0表示匹配成功,1表示REG_NOMATCH。
报错信息
size_t regerror(int errcode, const regex_t* preg, char* buf, size_t buffer_size);
参数说明
    errcode : 来自regcomp和regexec的错误码。
    preg : 编译后的正则表达式
    buf : 缓冲区错误信息字符串
    buffer_size : 缓冲区最大长度。

释放正则表达式

//释放reget_t指针,无返回值。
void regfree(reget_t* preg);


实例

c

include

include

include

int main() {
regex_t preg;
int iErr = 0;
regmatch_t subs[256];
char acReg[] = “[0-9a-zA-Z]+$”;

iErr = regcomp(&preg, acReg, REG_EXTENDED);

if (iErr) {
    printf("compile reg error\n");
    exit(1);
}

iErr = regexec(&preg, "12345", 256, subs, 0);

if (REG_NOMATCH == iErr) {
    printf("no match\n");
} else {
    printf("match\n");
}

}

# 8. 信号
## 8.1. 信号的机制
    A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。

    信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。每个进程收到的所有信号,都是由内核负责发送的,内核处理。与信号相关的事件和状态

    产生信号:
            按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\
            系统调用产生,如:kill、raise、abort
            软件条件产生,如:定时器alarm
            硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
            命令产生,如:kill命令
        递达:递送并且到达进程。
        未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。
        信号的处理方式:
            执行默认动作
            忽略(丢弃)
            捕捉(调用户处理函数)

        Linux内核的进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
        阻塞信号集(****信号屏蔽字): 将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)
        未决信号集:
            信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
            信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
    信号的编号
    可以使用kill –l命令查看当前系统可使用的信号有哪些。
    ```c
        SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
        SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
        SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
        SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
        SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
        SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
        SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
        SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
        SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
        SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
        SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
        SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
        SIGRTMAX-1 64) SIGRTMAX
    ```

不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前32个名字各不相同。


   信号4要素
   与变量三要素类似的,每个信号也有其必备4要素,分别是:
       1. 编号 2. 名称 3. 事件 4. 默认处理动作 

   可通过man 7 signal查看帮助文档获取。也可查看/usr/src/linux-headers-3.16.0-30/arch/s390/include/uapi/asm/signal.h
       Signal         Value     Action   Comment
   ────────────────────────────────────────────
   SIGHUP       1       Term    Hangup detected on controlling terminal or death of controlling process
   SIGINT        2       Term    Interrupt from keyboard
   SIGQUIT       3       Core    Quit from keyboard
   SIGILL         4       Core    Illegal Instruction
   SIGFPE        8       Core    Floating point exception
   SIGKILL        9       Term    Kill signal
   SIGSEGV       11      Core    Invalid memory reference
   SIGPIPE          13      Term    Broken pipe: write to pipe with no readers
   SIGALRM       14      Term    Timer signal from alarm(2)
   SIGTERM      15      Term    Termination signal
   SIGUSR1   30,10,16    Term    User-defined signal 1
   SIGUSR2   31,12,17    Term    User-defined signal 2
   SIGCHLD   20,17,18    Ign     Child stopped or terminated
   SIGCONT   19,18,25    Cont    Continue if stopped
   SIGSTOP   17,19,23    Stop    Stop process
   SIGTSTP   18,20,24    Stop    Stop typed at terminal
   SIGTTIN   21,21,26    Stop    Terminal input for background process
   SIGTTOU   22,22,27   Stop    Terminal output for background process
   The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.   

    在标准信号中,有一些信号是有三个“Value”,第一个值通常对alpha和sparc架构有效,中间值针对x86、arm和其他架构,最后一个应用于mips架构。一个‘-’表示在对应架构上尚未定义该信号。
    不同的操作系统定义了不同的系统信号。因此有些信号出现在Unix系统内,也出现在Linux中,而有的信号出现在FreeBSD或Mac OS中却没有出现在Linux下。这里我们只研究Linux系统中的信号。
默认动作:
                                 Term:终止进程
                                 Ign: 忽略信号 (默认即时对该种信号忽略操作)
                                 Core:终止进程,生成Core文件.(查验进程死亡原因,用于gdb调试)
                                 Stop:停止(暂停)进程
                                 Cont:继续运行进程


        注意从man 7 signal帮助文档中可看到 : The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
        这里特别强调了9) SIGKILL 和19) SIGSTOP****信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。
        另外需清楚,只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达),不应乱发信号!!

    Linux常规信号一览表
        SIGHUP: 当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程
        SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
        SIGQUIT:当用户按下<ctrl+>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。
        SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
        SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程 并产生core文件。
        SIGABRT: 调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
        SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
        SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
        SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
        SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
        SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。
        SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
        SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。
        SIGALRM: 定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程。
        SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。
        SIGSTKFLT:Linux早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
        SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。
        SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。
        SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
        SIGTSTP:停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
        SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。
        SIGTTOU: 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
        SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
        SIGXCPU:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。
        SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。
        SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。
        SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。
        SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。
        SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。
        SIGPWR:关机。默认动作为终止进程。
        SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。
        SIGRTMIN ~ (64) SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。



## 8.2. 第二章 信号的产生

    终端按键产生信号

     Ctrl + c → 2) SIGINT(终止/中断) “INT” ----Interrupt
     Ctrl + z  → 20) SIGTSTP(暂停/停止)  "T" ----Terminal 终端。
     Ctrl + \  → 3) SIGQUIT(退出) 
    硬件异常产生信号
    除0操作   → 8) SIGFPE (浮点数例外)     "F" -----float 浮点数。
    非法访问内存  → 11) SIGSEGV (段错误)
    总线错误  → 7) SIGBUS     

    kill函数/命令产生信号
    kill命令产生信号:kill -SIGKILL pid
    kill函数:给指定进程发送指定信号(不一定杀死)

    /**
    * pid: [>0(发送信号给指定的进程)|=0(发送信号给 与调用kill函数进程属于同一进程组的所有进程)|<0(取pid发给对应进程组)|=-1(发送给进程有权限发送的系统中所有进程)]
    * sig:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致
    * 成功:0;失败:-1 (ID非法,信号非法,普通用户杀init进程等权级问题),设置errno
    */
    int kill(pid_t pid, int sig);  
    进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。


            权限保护:super用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。 kill -9 (root用户的pid) 是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。 只能向自己创建的进程发送信号。普通用户基本规则是:发送者实际或有效用户ID == 接收者实际或有效用户ID
    练习:循环创建5个子进程,任一子进程用kill函数终止其父进程。
    raise和abort函数
    raise 函数:给当前进程发送指定信号(自己给自己发)   raise(signo) == kill(getpid(), signo);
    /**
    * 成功:0,失败非0值
    */
    int raise(int sig);
       abort 函数:给自己发送异常终止信号 6) SIGABRT 信号,终止并产生core文件
    /**
    * 该函数无返回
    */
    void abort(void);
    软件条件产生信号

        alarm函数
        设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。
        每个进程都有且只有唯一个定时器。

        /**
        * 返回0或剩余的秒数,无失败。
        * /
        unsigned int alarm(unsigned int seconds); 
        //常用:取消定时器alarm(0),返回旧闹钟余下秒数。
        //例:alarm(5) → 3sec → alarm(4) → 5sec → alarm(5) → alarm(0)

        定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸...无论进程处于何种状态,alarm都计时。

        练习:编写程序,测试你使用的计算机1秒钟能数多少个数。 【alarm .c】

           使用time命令查看程序执行的时间。      程序运行的瓶颈在于IO,优化程序,首选优化IO。         
           实际执行时间 = 系统时间 + 用户时间 + 等待时间

        setitimer函数
        设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
        /**
        * which:指定定时方式 ① 自然定时:ITIMER_REAL → 14)SIGLARM 计算自然时间 ② 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM,只计算进程占用cpu的时间 ③ 运行时计时(用户+内核):ITIMER_PROF → 27)SIGPROF,计算占用cpu及执行系统调用的时间
        * 成功:0;失败:-1,设置errno
        */
        int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

        练习: 使用setitimer函数实现alarm函数,重复计算机1秒数数程序。 【setitimer.c】

        拓展练习,结合man page编写程序,测试it_interval、it_value这两个参数的作用。 【setitimer1.c】
        提示: it_interval:用来设定两次定时任务之间间隔的时间。it_value:定时的时长。两个参数都设置为0,即清0操作。

## 8.3. 第三章 信号集操作函数

        内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字mask可以影响未决信号集。而我们可以在应用程序中自定义set来改变mask。已达到屏蔽指定信号的目的。

    信号集设定
    sigset_t  set;  //typedef unsigned long sigset_t; 

    /**
    * 将某个信号集清0
    * 返回值:成功:0;失败:-1
    */
    int sigemptyset(sigset_t *set);                       

    /**
    * 将某个信号集置1
    * 返回值:成功:0;失败:-1
    */
    int sigfillset(sigset_t *set);                                              

    /**
    * 将某个信号加入信号集
    * 返回值:成功:0;失败:-1
    */
    int sigaddset(sigset_t *set, int signum);                    

    /**
    * 将某个信号清出信号集
    * 返回值:成功:0;失败:-1
    */
    int sigdelset(sigset_t *set, int signum);         

    /**
    * 判断某个信号是否在信号集中
    * 返回值:在集合:1;不在:0;出错:-1 
    */
    int sigismember(const sigset_t *set, int signum);   

    //对比认知select 函数。
            sigset_t类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
    sigprocmask函数

            用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB中)
            严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢处理。

    /**
    * how参数取值:   假设当前的信号屏蔽字为mask [SIG_BLOCK(当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set) | SIG_UNBLOCK(当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set) | SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于 mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达]
    * set:传入参数,是一个位图,set中哪位置1,就表示当前进程屏蔽哪个信号。
    * oldset:传出参数,保存旧的信号屏蔽集。
    * 成功:0;失败:-1,设置errno
    */
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    sigpending函数
    读取当前进程的未决信号集
    /**
    * set传出参数。 
    * 返回值:成功:0;失败:-1,设置errno
    */
    int sigpending(sigset_t *set); 
    练习:把所有常规信号的未决状态打印至屏幕。 【sigpending.c】

## 8.4. 第四章 信号捕捉

    signal函数

    注册一个信号捕捉函数:

    typedef void (*sighandler_t)(int);

    //该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。
    sighandler_t signal(int signum, sighandler_t handler);

    void (*signal(int signum, void (*sighandler_t)(int))) (int);
        sigaction函数
        修改信号处理动作(通常在Linux用其来注册一个信号的捕捉函数)


## 8.5. 第五章 竞态条件(时序竞态)

    pause函数

           调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(主动放弃cpu) 直到有信号递达将其唤醒。

    /**
    *返回值:-1 并设置errno为EINTR
    */
    int pause(void);
    //返回值:
    //① 如果信号的默认处理动作是终止进程,则进程终止,pause函数么有机会返回。
    //② 如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。
    //③ 如果信号的处理动作是捕捉,则【调用完信号处理函数之后,pause返回-1】 errno设置为EINTR,表示“被信号中断”。想想我们还有哪个函数只有出错返回值。
    //④ pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。

    练习:使用pause和alarm来实现sleep函数。 【mysleep.c】
    注意,unslept = alarm(0)的用法。
    例如:睡觉,alarm(10)闹铃。
    正常: 10后闹铃将我唤醒,这时额外设置alarm(0)取消闹铃,不会出错。
    异常: 5分钟,被其他事物吵醒,alarm(0)取消闹铃防止打扰。
    时序竞态
        前导例
            设想如下场景:
            欲睡觉,定闹钟10分钟,希望10分钟后闹铃将自己唤醒。
            正常:定时,睡觉,10分钟后被闹钟唤醒。
            异常:闹钟定好后,被唤走,外出劳动,20分钟后劳动结束。回来继续睡觉计划,但劳动期间闹钟已经响过,不会再将我唤醒。

        时序问题分析

        回顾,借助pause和alarm实现的mysleep函数。设想如下时序:

                注册SIGALRM信号处理函数 (sigaction…)
                调用alarm(1) 函数设定闹钟1秒。
                函数调用刚结束,开始倒计时1秒。当前进程失去cpu,内核调度优先级高的进程(有多个)取代当前进程。当前进程无法获得cpu,进入就绪态等待cpu。
                1秒后,闹钟超时,内核向当前进程发送SIGALRM信号(自然定时法,与进程状态无关),高优先级进程尚未执行完,当前进程仍处于就绪态,信号无法处理(未决)
                优先级高的进程执行完,当前进程获得cpu资源,内核调度回当前进程执行。SIGALRM信号递达,信号设置捕捉,执行处理函数sig_alarm。
                信号处理函数执行结束,返回当前进程主控流程,pause()被调用挂起等待。(欲等待alarm函数发送的SIGALRM信号将自己唤醒)
                SIGALRM信号已经处理完毕,pause不会等到。

        解决时序问题

               可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽”与“挂起等待信号”这个两个操作间隙失去cpu资源。除非将这两步骤合并成一个“原子操作”。sigsuspend函数具备这个功能。在对时序要求严格的场合下都应该使用sigsuspend替换pause。

        /**
        * 挂起等待信号。
        * /
        int sigsuspend(const sigset_t *mask);   
                 //sigsuspend函数调用期间,进程信号屏蔽字由其**参数mask**指定。


              可将某个信号(如SIGALRM)从临时信号屏蔽字mask中删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽态,sigsuspend函数返回后仍然屏蔽该信号。

              改进版mysleep 【sigsuspend.c】
        总结

              竞态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。

              不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否应处理某个信号。当系统负载过重时,会出现时序混乱。
              这种意外情况只能在编写程序过程中,提早预见,主动规避,而无法通过gdb程序调试等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。
    全局变量异步I/O

           分析如下父子进程交替数数程序。当捕捉函数里面的sleep取消,程序即会出现问题。请分析原因。

c
//【sync_process.c】
#include
#include
#include
#include

int n = 0, flag = 0;
void sys_err(char *str){
    perror(str);
    exit(1);
}
void do_sig_child(int num){
    printf("I am child  %d\t%d\n", getpid(), n);
    n += 2;
    flag = 1;
    sleep(1);
}
void do_sig_parent(int num){
    printf("I am parent %d\t%d\n", getpid(), n);
    n += 2;
    flag = 1;
    sleep(1);
}
int main(void){
    pid_t pid;
        struct sigaction act;

    if ((pid = fork()) < 0)
        sys_err("fork");
    else if (pid > 0) {     
        n = 1;
        sleep(1);
        act.sa_handler = do_sig_parent;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGUSR2, &act, NULL); //注册自己的信号捕捉函数  父使用SIGUSR2信号
        do_sig_parent(0);                          
        while (1) {
            /* wait for signal */;
           if (flag == 1) {        //父进程数数完成
                kill(pid, SIGUSR1);
                flag = 0;   //标志已经给子进程发送完信号
            }
        }
 } else if (pid == 0) {       
        n = 2;
        act.sa_handler = do_sig_child;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGUSR1, &act, NULL);

        while (1) {
            /* waiting for a signal */;
            if (flag == 1) {
                kill(getppid(), SIGUSR2);
                flag = 0;
            }
        }
    }
    return 0;
}    
       示例中,通过flag变量标记程序实行进度。flag置1表示数数完成。flag置0表示给对方发送信号完成。
        问题出现的位置,在父子进程kill函数之后需要紧接着调用 flag,将其置0,标记信号已经发送。但,在这期间很有可能被kernel调度,失去执行权利,而对方获取了执行时间,通过发送信号回调捕捉函数,从而修改了全局的flag。
        如何解决该问题呢?可以使用后续课程讲到的“锁”机制。当操作全局变量的时候,通过加锁、解锁来解决该问题。
        现阶段,我们在编程期间如若使用全局变量,应在主观上注意全局变量的异步****IO可能造成的问题。


## 8.6. signal与sigaction
要对一个信号进行处理,就需要给出此信号发生时系统所调用的处理函数。可以对一个特定的信号(除去SIGKILL和SIGSTOP信号)注册相应的处理函数。注册某个信号的处理函数后,当进程接收到此信号时,无论进程处于何种状态,就会停下当前的任务去执行此信号的处理函数。

### 8.6.1. 、注册信号函数。

c
#include

void(*signal(int signumber,void ((*func)(int))(int)  
signumber表示信号处理函数对应的信号。func是一个函数指针。此函数有一整型参数,并返回void型。其实func还可以取其他定值如:SIG_IGN,SIG_DFL.
SIG_IGN表示:忽略signumber所指出的信号。SIG_DFL表示表示调用系统默认的处理函数。signal函数的返回值类型同参数func,是一个指向某个返回值为空并带有一个整型参数的函数指针。其正确返回值应为上次该信号的处理函数。错误返回SIG_ERR
signal示例如下:

c
#include
#include
#include
#include
void func(int sig)
{
printf(“I get asignal!\n”);
}
int main()
{ charbuffer[100];

   if(signal(SIGINT, func) == SIG_ERR)  
     {  
     printf("signalerror exit now\n");  
     exit(0);  
     }  
     printf("pid:%ld\n",(long)getpid());  
   for(;;)  
     {  
     fgets(buffer,sizeof(buffer),stdin);  
     printf("bufferis:%s\n",buffer);  
     }  
 return 0;   
}   
通常情况下一个用户进程需要处理多个信号。可以在一个程序中注册多个信号处理函数。一个信号可以对应一个处理函数,同时多个信号可以对应一个处理函数。
对于SIGINT信号 我们可以用ctrl+c或ctrl+z来中断进程,来执行SIGINT注册的函数。


### 8.6.2. 、 高级信号处理。

在linux系统提供了一个功能更强的系统调用。

c
#include
int sigaction(int signumbet,const structsigaction *act,struct sigaction *oldact);

 此函数除能注册信号函数外还提供了更加详细的信息,确切了解进程接收到信号,发生的具体细节。
struct sigaction的定义如下:在linux2.6.39/include/asm-generic/signal.h中实现

    struct sigaction   
    {   
         void(*sa_handler)(int);  
         void(*sa_sigaction)(int,siginfo_t *,void *);  
         sigset_tsa_mask;  
         intsa_flags;  
    }  

siginfo_t在linux2.6.39/include/asm-generic/siginfo.h中实现:
sa_flags的取值如下表,取0表示选用所有默认选项
SA_NOCLDSTOP:用于表示信号SIGCHLD,当子进程被中断时,不产生此信号,当且仅当子进程结束时产生此信号
SA_NOCLDWATI:当信号为SIGCHLD,时可避免子进程僵死。
SA_NODEFER:当信号处理函数正在进行时,不堵塞对于信号处理函数自身信号功能。
SA_NOMASK:同SA_NODEFER
SA_ONESHOT:当用户注册的信号处理函数被执行过一次后,该信号的处理函数被设为系统默认的处理函数。
SA_RESETHAND:同SA_ONESHOT
SA_RESTART:是本来不能重新于运行的系统调用自动重新运行。
SA_SIGINFO:表明信号处理函数是由SA_SIGACTION指定的,而不是由SA_HANDLER指定的,它将显示更多的信号处理函数信息。
其实sinaction完全可以替换signal函数

c
#include
#include
#include
#include

void func(int sig)    
{  
printf("I get a signal!\n");   
}    
int main()    
{   
    char buffer[100];    
    struct sigaction act;  
    act.sa_handler=func;  
    sigemptyset(&act.sa_mask);  
    act.sa_flags = 0;  
    if(sigaction(SIGINT,&act, NULL) == -1)  
    {  
    printf("sigaction error exit now\n");  
    exit(0);  
    }  
    printf("pid:%ld\n",(long)getpid());   
    for(;;)  
    {  
    fgets(buffer,sizeof(buffer),stdin);  
    printf("buffer is:%s\n",buffer);  
    }  
    return 0;    
}    

“`

9. 附表1 :POSIX定义头文件

POSIX标准定义的头文件
目录项
文件控制
文件名匹配类型
路径名模式匹配类型
组文件
网络数据库操作
口令文件
正则表达式
TAR归档值
终端I/O
符号常量
文件时间
字符扩展类型
————————-
INTERNET定义
套接字本地接口
INTERNET地址族
传输控制协议定义
————————-
内存管理声明
Select函数
套接字借口
文件状态
进程时间
基本系统数据类型
UNIX域套接字定义
系统名
进程控制
——————————
POSIX定义的XSI扩展头文件
cpio归档值
动态链接
消息显示结构
文件树漫游
代码集转换使用程序
语言信息常量
模式匹配函数定义
货币类型
数据库操作
消息类别
轮询函数
搜索表
字符串操作
系统出错日志记录
用户上文
用户限制
用户帐户数据库
—————————–
IPC(命名管道)
消息队列
资源操作
信号量
共享存储
文件系统信息
时间类型
附加的日期和时间定义
矢量I/O操作
——————————
POSIX定义的可选头文件
异步I/O
消息队列
线程
执行调度
信号量
实时spawn接口
XSI STREAMS接口
事件跟踪

10. 附表2 LINUX ERRNO

errno: 0 Success
errno: 1 Operation not permitted
errno: 2 No such file or directory
errno: 3 No such process
errno: 4 Interrupted system call
errno: 5 Input/output error
errno: 6 No such device or address
errno: 7 Argument list too long
errno: 8 Exec format error
errno: 9 Bad file descriptor
errno: 10 No child processes
errno: 11 Resource temporarily unavailable
errno: 12 Cannot allocate memory
errno: 13 Permission denied
errno: 14 Bad address
errno: 15 Block device required
errno: 16 Device or resource busy
errno: 17 File exists
errno: 18 Invalid cross-device link
errno: 19 No such device
errno: 20 Not a directory
errno: 21 Is a directory
errno: 22 Invalid argument
errno: 23 Too many open files in system
errno: 24 Too many open files
errno: 25 Inappropriate ioctl for device
errno: 26 Text file busy
errno: 27 File too large
errno: 28 No space left on device
errno: 29 Illegal seek
errno: 30 Read-only file system
errno: 31 Too many links
errno: 32 Broken pipe
errno: 33 Numerical argument out of domain
errno: 34 Numerical result out of range
errno: 35 Resource deadlock avoided
errno: 36 File name too long
errno: 37 No locks available
errno: 38 Function not implemented
errno: 39 Directory not empty
errno: 40 Too many levels of symbolic links
errno: 41 Unknown error
errno: 42 No message of desired type
errno: 43 Identifier removed
errno: 44 Channel number out of range
errno: 45 Level 2 not synchronized
errno: 46 Level 3 halted
errno: 47 Level 3 reset
errno: 48 Link number out of range
errno: 49 Protocol driver not attached
errno: 50 No CSI structure available
errno: 51 Level 2 halted
errno: 52 Invalid exchange
errno: 53 Invalid request descriptor
errno: 54 Exchange full
errno: 55 No anode
errno: 56 Invalid request code
errno: 57 Invalid slot
errno: 58 Unknown error 58
errno: 59 Bad font file format
errno: 60 Device not a stream
errno: 61 No data available
errno: 62 Timer expired
errno: 63 Out of streams resources
errno: 64 Machine is not on the network
errno: 65 Package not installed
errno: 66 Object is remote
errno: 67 Link has been severed
errno: 68 Advertise error
errno: 69 Srmount error
errno: 70 Communication error on send
errno: 71 Protocol error
errno: 72 Multihop attempted
errno: 73 RFS specific error
errno: 74 Bad message
errno: 75 Value too large for defined data type
errno: 76 Name not unique on network
errno: 77 File descriptor in bad state
errno: 78 Remote address changed
errno: 79 Can not access a needed shared library
errno: 80 Accessing a corrupted shared library
errno: 81 .lib section in a.out corrupted
errno: 82 Attempting to link in too many shared libraries
errno: 83 Cannot exec a shared library directly
errno: 84 Invalid or incomplete multibyte or wide character
errno: 85 Interrupted system call should be restarted
errno: 86 Streams pipe error
errno: 87 Too many users
errno: 88 Socket operation on non-socket
errno: 89 Destination address required
errno: 90 Message too long
errno: 91 Protocol wrong type for socket
errno: 92 Protocol not available
errno: 93 Protocol not supported
errno: 94 Socket type not supported
errno: 95 Operation not supported
errno: 96 Protocol family not supported
errno: 97 Address family not supported by protocol
errno: 98 Address already in use
errno: 99 Cannot assign requested address
errno: 100 Network is down
errno: 101 Network is unreachable
errno: 102 Network dropped connection on reset
errno: 103 Software caused connection abort
errno: 104 Connection reset by peer
errno: 105 No buffer space available
errno: 106 Transport endpoint is already connected
errno: 107 Transport endpoint is not connected
errno: 108 Cannot send after transport endpoint shutdown
errno: 109 Too many references: cannot splice
errno: 110 Connection timed out
errno: 111 Connection refused
errno: 112 Host is down
errno: 113 No route to host
errno: 114 Operation already in progress
errno: 115 Operation now in progress
errno: 116 Stale file handle
errno: 117 Structure needs cleaning
errno: 118 Not a XENIX named type file
errno: 119 No XENIX semaphores available
errno: 120 Is a named type file
errno: 121 Remote I/O error
errno: 122 Disk quota exceeded
errno: 123 No medium found
errno: 124 Wrong medium type
errno: 125 Operation canceled
errno: 126 Required key not available
errno: 127 Key has expired
errno: 128 Key has been revoked
errno: 129 Key was rejected by service
errno: 130 Owner died
errno: 131 State not recoverable
errno: 132 Operation not possible due to RF-kill
errno: 133 Memory page has hardware error
errno: 134~255 unknown error!

【LINUX驱动框架学习】Linux GPIO驱动以及底层实现方式

GPIO(通用目的输入/输出端口)是一种灵活的软件控制的数字信号。大多数的嵌入式
处理器都引出一组或多组的 GPIO,并且部分普通管脚通过配置可以复用为 GPIO。利用可
编程逻辑器件,或总线(如 I 2 C、SPI)转 GPIO 芯片,也可以扩展系统的 GPIO。不管是何
种 GPIO,GPIOLIB 为内核和用户层都提供了标准的操作方法。
GPIOLIB 的接口十分简洁。在 GPIOLIB,所有的 GPIO 都是用整形的 GPIO 编号标识。
只要获得要操作 GPIO 的编号,就可以调用 GPIOLIB 提供的方法操作 GPIO。

GPIOLIB 的内核接口

GPIOLIB 的内核接口是指:若某些 GPIO 在 GPIOLIB 框架下被驱动后,GPIOLIB 为内
核的其它代码操作该 GPIO 而提供的标准接口。

1. GPIO 的申请和释放

GPIO 在使用前,必须先调用 gpio_request()函数申请 GPIO:

int gpio_request(unsigned gpio, const char *label);

该函数的 gpio 参数为 GPIO 编号;label 参数为 GPIO 的标识字符串,可以随意设定。
若该函数调用成功,将返回 0 值;否则返回非 0 值。gpio_request()函数调用失败的原因可能
为 GPIO 的编号不存在,或在其它地方已经申请了该 GPIO 编号而还没有释放。
当 GPIO 使用完成后,应当调用 gpio_free()函数释放 GPIO:
void gpio_free(unsigned gpio);

2. GPIO 的输出控制

在操作 GPIO 输出信号前,需要调用 gpio_direction_output()函数把 GPIO 设置为输出方
向:

int gpio_direction_output(unsigned gpio, int value);

把 GPIO 设置为输出方向后,参数 value 为默认的输出电平:1 为高电平;0 为低电平。
GPIO 被设置为输出方向后,就可以调用 gpio_set_value()函数控制 GPIO 输出高电平或
低电平:

void gpio_set_value(unsigned gpio, int value);

该函数的 value 参数可取值为:1 为高电平;0 为低电平。

3. GPIO 的输入控制

当需要从 GPIO 读取输入电平状态前,需要调用 gpio_direction_input()函数设置 GPIO 为
输入方向:

int gpio_direction_input(unsigned gpio);

在 GPIO 被设置为输入方向后,就可以调用 gpio_get_value()函数读取 GPIO 的输入电平
状态:

int gpio_get_value(unsigned gpio);

该函数的返回值为 GPIO 的输入电平状态:1 为高电平;0 为低电平。

4. GPIO 的中断映射

大多数的嵌入式处理器的 GPIO 引脚在被设置为输入方向后,可以用于外部中断信号的
输入。这些中断号和 GPIO 编号通常有对应关系,因此 GPIOLIB 为这些 GPIO 提供了
gpio_to_irq()函数用于通过 GPIO 编号而获得该 GPIO 中断号:

int gpio_to_irq(unsigned gpio);

gpio_to_irq()函数调用完成后,返回 GPIO 中断号。
由于并不是所有的 GPIO 都可以作为外部中断信号输入端口,所以 gpio_to_irq()函数不
是对所有的 GPIO 都强制实现的

基于这些函数可以在屏蔽底层寄存器的情况下写驱动

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/ioctl.h>
#include <linux/delay.h>
#include <linux/bcd.h>
#include <linux/capability.h>
#include <linux/rtc.h>
#include <linux/cdev.h>
#include <linux/miscdevice.h>
#include <linux/gpio.h>
#include <linux/slab.h>

#include <../arch/arm/mach-mx28/mx28_pins.h>
#include "gpio_driver.h"


struct gpio_info {
    u32            pin;
    char           pin_name[20];
    struct miscdevice *pmiscdev;
};

static struct gpio_info *gpio_info_file[255];

static struct gpio_info *all_gpios_info;

static struct gpio_info all_gpios_info_283[] ={
        {PINID_AUART0_CTS,   "gpio-DURX",       NULL},
        {PINID_AUART0_RTS,   "gpio-DUTX",       NULL},
        {PINID_AUART0_RX,    "gpio-URX0",       NULL},
        {PINID_AUART0_TX,    "gpio-UTX0",       NULL},
        {PINID_AUART1_RX,    "gpio-URX1",       NULL},
        {PINID_AUART1_TX,    "gpio-UTX1",       NULL},
        {PINID_SSP2_SCK,     "gpio-URX2",       NULL},
        {PINID_SSP2_MOSI,    "gpio-UTX2",       NULL},
        {PINID_SSP2_MISO,    "gpio-URX3",       NULL},
        {PINID_SSP2_SS0,     "gpio-UTX3",       NULL},
        {PINID_SAIF0_BITCLK, "gpio-URX4",       NULL},
        {PINID_SAIF0_SDATA0, "gpio-UTX4",       NULL},
        /* modify by luozhizhuo*/
        {PINID_GPMI_RDY3,    "gpio-CRX0",     NULL},
        {PINID_GPMI_RDY2,    "gpio-CTX0",     NULL},
        {PINID_GPMI_CE3N,    "gpio-CRX1",     NULL},
        {PINID_GPMI_CE2N,    "gpio-CTX1",     NULL},
        {PINID_LCD_D22,      "gpio-RUN",        NULL},
        {PINID_LCD_D23,      "gpio-ERR",        NULL},
        /*end modify*/
        {PINID_SSP3_MISO,    "gpio-MISO",       NULL},
        {PINID_SSP3_MOSI,    "gpio-MOSI",       NULL},
        {PINID_SSP3_SCK,     "gpio-CLK",        NULL},
        {PINID_SSP3_SS0,     "gpio-CS",         NULL},
        {PINID_PWM1,         "gpio-SDA",        NULL},
        {PINID_PWM0,         "gpio-SCL",        NULL},
        {PINID_LCD_D17,      "gpio-P1.17",      NULL},
        {PINID_LCD_D18,      "gpio-P1.18",      NULL},
        {PINID_SSP0_DATA4,   "gpio-P2.4",       NULL},
        {PINID_SSP0_DATA5,   "gpio-P2.5",       NULL},
        {PINID_SSP0_DATA6,   "gpio-P2.6",       NULL},
        {PINID_SSP0_DATA7,   "gpio-P2.7",       NULL},
        {PINID_SSP1_SCK,     "gpio-P2.12",      NULL},
        {PINID_SSP1_CMD,     "gpio-P2.13",      NULL},
        {PINID_SSP1_DATA0,   "gpio-P2.14",      NULL},
        {PINID_SSP1_DATA3,   "gpio-P2.15",      NULL},
        {PINID_SAIF0_MCLK,   "gpio-P3.20",      NULL},
        {PINID_SAIF0_LRCLK,  "gpio-P3.21",      NULL},
        {PINID_SAIF1_SDATA0, "gpio-P3.26",      NULL},
        {PINID_SPDIF,        "gpio-P3.27",      NULL},
        {0,                "",          NULL},   //the end
};


/*--------------------------------------------------------------------------------------------------------
*/
static int gpio_open(struct inode *inode, struct file *filp);
static int  gpio_release(struct inode *inode, struct file *filp);
ssize_t gpio_write(struct file *filp, const char __user *buf, size_t count,
                loff_t *f_pos);
static int gpio_ioctl(struct inode *inode,struct file *flip,unsigned int command,unsigned long arg);
static int gpio_init(void);
static void gpio_exit(void);

/*--------------------------------------------------------------------------------------------------------
*/

static int gpio_open(struct inode *inode, struct file *filp)
{
    struct gpio_info *gpio_info_tmp;
    u32 minor = iminor(inode);

    gpio_info_tmp = gpio_info_file[minor];

    gpio_free(MXS_PIN_TO_GPIO(gpio_info_tmp->pin));
    if (gpio_request(MXS_PIN_TO_GPIO(gpio_info_tmp->pin), gpio_info_tmp->pin_name)) {
        printk("request %s gpio faile \n", gpio_info_tmp->pin_name);
        return -1;
    }

    filp->private_data = gpio_info_file[minor];

    return 0;
}

static int  gpio_release(struct inode *inode, struct file *filp)
{
    struct gpio_info *gpio_info_tmp = (struct gpio_info *)filp->private_data;

    gpio_free(MXS_PIN_TO_GPIO(gpio_info_tmp->pin));

    return 0;
}


ssize_t gpio_write(struct file *filp, const char __user *buf, size_t count,
                loff_t *f_pos)
{
    struct gpio_info *gpio_info_tmp = (struct gpio_info *)filp->private_data;
    char data[2];

    //printk("make: %s \n", gpio_info_tmp->pin_name);
    copy_from_user(data, buf, 2);

    data[0] = data[0] - '0';

        if (data[0] == 1 || data[0] == 0) {
                gpio_direction_output(MXS_PIN_TO_GPIO(gpio_info_tmp->pin), data[0]);
        }



    return count;
}


static ssize_t gpio_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    struct gpio_info *gpio_info_tmp = (struct gpio_info *)file->private_data;
    int  value = 0;
    char data[3];
    static int flg  = 0;

    if (flg == 1) {
        flg = 0;
        return 0;
    }

    gpio_direction_input(MXS_PIN_TO_GPIO(gpio_info_tmp->pin));
    value = gpio_get_value(MXS_PIN_TO_GPIO(gpio_info_tmp->pin));
    data[0] = value ? 1 : 0;

    data[0] = data[0] + '0';
    data[1] = '\n';
    data[3] = -1;

    copy_to_user(buf, data, 2);

    flg = 1;
        return 3;
}

static int gpio_ioctl(struct inode *inode,struct file *flip,unsigned int command,unsigned long arg)
{
    struct gpio_info *gpio_info_tmp = (struct gpio_info *)flip->private_data;
    int  data = 0;

    switch (command) {
    case SET_GPIO_HIGHT: 
        gpio_direction_output(MXS_PIN_TO_GPIO(gpio_info_tmp->pin), 1);
        break;
    case SET_GPIO_LOW:
        gpio_direction_output(MXS_PIN_TO_GPIO(gpio_info_tmp->pin), 0);
        break;
    case GET_GPIO_VALUE:
        gpio_direction_input(MXS_PIN_TO_GPIO(gpio_info_tmp->pin));
        data = gpio_get_value(MXS_PIN_TO_GPIO(gpio_info_tmp->pin));
        data = data ? 1 : 0;
        copy_to_user((void *)arg, (void *)(&data), sizeof(int));
        break;
    default:
        printk("cmd error \n");

        return -1;
    }

    return 0;
}


static struct file_operations gpio_fops={
    .owner      = THIS_MODULE,
    .open       = gpio_open,
    .write      = gpio_write,
    .read       = gpio_read,
    .release    = gpio_release,
    .ioctl      = gpio_ioctl,
};



static int __init gpio_init(void)
{
    int i = 0;
    int ret = 0;

    all_gpios_info = all_gpios_info_283;

    for (i = 0; all_gpios_info[i].pin != 0; i++) {
        all_gpios_info[i].pmiscdev = kmalloc(sizeof(struct miscdevice), GFP_KERNEL);
        if (all_gpios_info[i].pmiscdev == NULL) {
            printk("unable to malloc memory \n");
            return -1;
        }


        memset(all_gpios_info[i].pmiscdev, 0, sizeof(struct miscdevice));
        all_gpios_info[i].pmiscdev->name  = all_gpios_info[i].pin_name;
        all_gpios_info[i].pmiscdev->fops  = &gpio_fops; 
        all_gpios_info[i].pmiscdev->minor = MISC_DYNAMIC_MINOR;

        ret = misc_register(all_gpios_info[i].pmiscdev);
        if (ret) {
            printk("misc regist faile \n");
            return -1;
        }

        gpio_info_file[all_gpios_info[i].pmiscdev->minor] = &(all_gpios_info[i]);

        printk("build device i:%d dev:/dev/%s \n", i, all_gpios_info[i].pmiscdev->name);
    }
        printk("zlg EasyARM-imx283 gpio driver up. \n");


    return 0;
}

static void __exit gpio_exit(void)
{
    int i = 0;

    for (i = 0; all_gpios_info[i].pin != 0; i++) {
        misc_deregister(all_gpios_info[i].pmiscdev);
    }
    printk("zlg EasyARM-imx28xx gpio driver down.\n");
}

module_init(gpio_init);
module_exit(gpio_exit);

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("zhuguojun, ZhiYuan Electronics Co, Ltd.");
MODULE_DESCRIPTION("GPIO DRIVER FOR MAGICARM270.");

底层实现方式(以MX28为例)

对于GPIOLIB底层实现的源码位于drivers/gpiolib.c

GPIOLIB 对每组 GPIO 都用一个 gpio_chip 对象来实现其驱动。gpio_chip 也称为 GPIO
控制器,其定义

struct gpio_chip {
const char *label;
struct device  *dev;
struct module  *owner;
int  (*request)(struct gpio_chip *chip, unsigned offset);
void (*free)(struct gpio_chip *chip, unsigned offset);
int  (*direction_input)(struct gpio_chip *chip, unsigned offset);
int  (*get)(struct gpio_chip *chip, unsigned offset);
int  (*direction_output)(struct gpio_chip *chip, unsigned offset, int value);
int  (*set_debounce)(struct gpio_chip *chip, unsigned offset, unsigned debounce);
void (*set)(struct gpio_chip *chip, unsigned offset, int value);
int  (*to_irq)(struct gpio_chip *chip, unsigned offset);
void (*dbg_show)(struct seq_file *s, struct gpio_chip *chip);
int  base;
u16 ngpio;
const char *const *names;
unsigned  can_sleep:1;
unsigned  exported:1;
};

下面介绍 gpio_chip 的部分成员:
base 该组 GPIO 的起始值。
ngpio 该组 GPIO 的数量。
owner 该成员表示所有者,一般设置为 THIS_MODULE。
request在对该组的GPIO调用gpio_request()函数时,该成员指向的实现函数将被调用。
在该成员指向的实现函数中,通常需要执行指定 GPIO 的初始化操作。
在实现函数中都是用索引值来区别组内的 GPIO。索引值是指组内的某一 GPIO 编号相
对于该组 GPIO 起始值(base)的偏移量,例如,组内第 1 个 GPIO 的索引值为 0、第 2 个
GPIO 的索引值为 1„„ 实现函数的 offset 参数为要操作 GPIO 的索引值(以下相同)。
free 在对该组的 GPIO 调用 gpio_free()函数时,该成员指向的实现函数将被调用。在该
成员的实现函数中,通常需要执行指定 GPIO 硬件资源的释放操作。
direction_input 在对该组的 GPIO 调用 gpio_direction_input()函数时,该成员指向的实
现函数将被调用。在该成员的实现函数中,需要把指定的 GPIO 设置为输入方向。
get 在对该组的 GPIO 调用 gpio_get_value()函数时,该成员指向的实现函数将被调用。
在该成员的实现函数中,需要返回指定 GPIO 的电平输入状态。
direction_output 在对该组的 GPIO 调用 gpio_direction_output()函数时,该成员指向的实
现函数将被调用。在该成员的实现函数中,需要把指定的 GPIO 设置为输出方向。
set 在对该组的 GPIO 调用 gpio_set_value()函数时,该成员指向的实现函数将被调用。
在该成员的实现函数中,需要把指定的 GPIO 设置为指定的电平输出状态。

以mx28为例查看如何实现gpiolib
mx28

gpio是以数字序号为索引的IMX283内部驱动实现了一个1-160 5*32的gpiolib
所以外部接口函数都可以通过一个数字访问到IO口

通过下面这个结构体管理gpio_chip
struct mxs_gpio_port {
int id;
int irq;
int child_irq;
struct mxs_gpio_chip *chip;
struct gpio_chip port;
};

可以通过下面这个函数将gpio_chip添加到内核中
int __init mxs_add_gpio_port(struct mxs_gpio_port *port)
{
int i, ret;
if (!(port && port->chip))
return -EINVAL;

if (mxs_valid_gpio(port))
    return -EINVAL;

if (mxs_gpios[port->id])
    return -EBUSY;

mxs_gpios[port->id] = port;

port->port.base = port->id * PINS_PER_BANK;
port->port.ngpio = PINS_PER_BANK;
port->port.can_sleep = 1;
port->port.exported = 1;
port->port.to_irq = mxs_gpio_to_irq;
port->port.direction_input = mxs_gpio_input;
port->port.direction_output = mxs_gpio_output;
port->port.get = mxs_gpio_get;
port->port.set = mxs_gpio_set;
port->port.request = mxs_gpio_request;
port->port.free = mxs_gpio_free;
port->port.owner = THIS_MODULE;
ret = gpiochip_add(&port->port);
if (ret < 0)
    return ret;

if (port->child_irq < 0)
    return 0;

for (i = 0; i < PINS_PER_BANK; i++) {
    gpio_irq_chip.mask(port->child_irq + i);
    set_irq_chip(port->child_irq + i, &gpio_irq_chip);
    set_irq_handler(port->child_irq + i, handle_level_irq);
    set_irq_flags(port->child_irq + i, IRQF_VALID);
}
set_irq_chained_handler(port->irq, mxs_gpio_irq_handler);
set_irq_data(port->irq, port);
return ret;

};

自己实现一个GPIO控制器

这里为 GPIO3_4 和 GPIO3_5 安排的 GPIO 编号分别为 160 和 161。这两个 GPIO 在同
一个 GPIO 组里,该组的 GPIO 编号起始编号为 160。实现该组的 GPIO 控制器代码如程序

struct gpio_chip mx28_gpio_chip = {
.label = "example_gpio",
.owner  = THIS_MODULE,
.base = 160,
|  .ngpio  = 2,
.request  = mxs_gpio_request,
.free = mxs_gpio_free,
.direction_input = mxs_gpio_input,
.get = mxs_gpio_get,
.direction_output = mxs_gpio_output,
.set = mxs_gpio_set,
143
.exported = 1,
};
由于该组的 GPIO 编号范围已经超出了内核源码定义的最大值,所以必须把
MXS_ARCH_NR_GPIOS 宏定义的值改为足够大的值:
#define MXS_ARCH_NR_GPIOS (160 + 2)
然后编译内核,把新内核固件烧写到目标机中。
下面介绍 mx28_gpio_chip 各成员函数的实现:
## request 的实现
request 成员的实现函数为 mxs_gpio_request(),该函数的代码如程序清单 4.7 所示。当
内核对编号为 160 ~ 161 的 GPIO 调用 gpio_request()函数时,该函数将被调用。
程序清单 4.7 mxs_gpio_request()函数的实现代码
static int mxs_gpio_request(struct gpio_chip *chip, unsigned int pin)
{
void __iomem *addr = PINCTRL_BASE_ADDR;
pin += 4;
__raw_writel(0x3 << pin * 2, addr + HW_PINCTRL_MUXSEL6_SET);  /* set as GPIO  */
return 0;
}
pin 参数为要申请 GPIO 编号的索引值:对于编号为 160 的 GPIO,pin 的值为 0;对于
编号为 161 的 GPIO,pin 的值为 1。由于 GPIO3_4 和 GPIO3_5 在 HW_PINCTRL_MUXSEL6
寄存器的操作位分别在 8 ~ 9 和 10 ~ 11,所以在该函数中需要根据 GPIO 的索引值来设置
寄存器的相应位。
## direction_input 的实现
direction_input成员的实现函数为mxs_gpio_input(),该函数的代码如程序清单 4.8所示。
当内核对编号为 160 ~ 161 的 GPIO 调用 gpio_direction_input ()函数时,该函数将被调用。

static int mxs_gpio_input(struct gpio_chip *chip, unsigned int index)
{
void __iomem *base = PINCTRL_BASE_ADDR;
index += 4;
__raw_writel(1 << index, base + HW_PINCTRL_DOE3_CLR);
return 0;
}
index 参数为要操作 GPIO 的索引值。在该函数中,需要根据传入 GPIO 的索引值在
HW_PINCTRL_DOE3 寄存器的正确位设置为 0,以控制相应的 GPIO 为输入工作状态。
 
## get 的实现

get 成员的实现函数为 mxs_gpio_get(),该函数的代码如程序清单 4.9 所示。当内核对编
号为 160 ~ 161 的 GPIO 调用 gpio_get_value()函数时,该函数将被调用。
程序清单 4.9 mxs_gpio_get()函数的实现代码
static int mxs_gpio_get(struct gpio_chip *chip, unsigned int index)
{
unsigned int data;
void __iomem *base = PINCTRL_BASE_ADDR;
index += 4;
data = __raw_readl(base + HW_PINCTRL_DIN3);
return data & (1 << index);
}
在该函数中,需要根据传入GPIO的索引值从HW_PINCTRL_DIN3寄存器的正确位中,
获得 GPIO 的输入电位状态。
## direction_output 的实现
direction_output 成员的实现函数为 mxs_gpio_output(),该函数的代码如程序清单 4.10
所示。当内核对编号为 160 ~ 161 的 GPIO 调用 gpio_direction_output ()函数时,该函数将被
调用。
程序清单 4.10 mxs_gpio_output()函数的实现代码
static int mxs_gpio_output(struct gpio_chip *chip, unsigned int index, int v)
{
void __iomem *base = PINCTRL_BASE_ADDR;
index += 4;
__raw_writel(1 << index, base + HW_PINCTRL_DOE3_SET); /* 设置为输出工作模式  */
if (v) {  /* 当 v 为非 0 时, 设置 GPIO 输出高电平 */
__raw_writel(1 << index, base + HW_PINCTRL_DOUT3_SET);
} else { /* 当 v 为 0 时,设置 GPIO 输出低电平 */
__raw_writel(1 << index, base + HW_PINCTRL_DOUT3_CLR);
}
return 0;
}
参数 v 为 GPIO 被设置为输出工作模式后,默认的输出值:0 为输出低电平,非 0 为输
出高电平。在该函数中,需要根据输入 GPIO 的索引值,在 HW_PINCTRL_DOE3 寄存器把
GPIO 设置为输出工作状态,然后在 HW_PINCTRL_DOUT3 寄存器设置默认的输出电平。
##  set 的实现
set 成员的实现函数为 mxs_gpio_set(),该函数的实现代码如程序清单 4.11 所示。当内
核对编号为 160 ~ 161 的 GPIO 调用 gpio_set_value ()函数时,该函数将被调用。

程序清单 4.11 mxs_gpio_set()函数的实现代码
static void mxs_gpio_set(struct gpio_chip *chip, unsigned int index, int v)
{
void __iomem *base = PINCTRL_BASE_ADDR;
index += 4;
if (v) {  /* 设置 GPIO 输出高电平 */
__raw_writel(1 << index, base + HW_PINCTRL_DOUT3_SET);
} else { /* 设置 GPIO 输出低电平 */
__raw_writel(1 << index, base + HW_PINCTRL_DOUT3_CLR);
}
}
在该函数中,需要根据输入 GPIO 的索引值,在 HW_PINCTRL_DOUT3 寄存器设置输
出电平。
##  GPIO 控制器的注册和注销
在模块的初始化函数中,需要注册 GPIO 控制器,如程序清单 4.12 所示。
程序清单 4.12 注册 GPIO 控制器的实现代码
static int __init mx28_gpio_init(void)
{
int ret = 0;
ret = gpiochip_add(&mx28_gpio_chip);
if (ret) {
printk("add example gpio faile:%d \n", ret);
goto out;
}
printk("add example gpio success... \n");
out:
return ret;
}
module_init(mx28_gpio_init);

static void __exit mx28_gpio_exit(void)
{
gpiochip_remove(&mx28_gpio_chip);
printk("remove example gpio... \n");
}
module_exit(mx28_gpio_exit);

装载模块以后可以在sys/class/gpio/export目录下找到对应的设备文件

【单片机开发】STM32简易示波器开发

@[toc]

(一)前言

在这里插入图片描述

还记得之前因为个人需要,又不太想花钱买示波器,实现了一个简易的示波器。

这个示波器非常明显存在以下几个非常尴尬的问题:
1.对于ADC数据的处理方式还是基于查询模式
2.最高采样率无法控制,而且最高采样率可能都不会超过10K
3.供电方式有点尴尬,非常不方便
4.无法提供基本信息,包括峰峰值,以及信号频率

在这里插入图片描述

基于这些问题我想到了以下的改进方案
1.ADC采集基于定时器触发模式,可以实现采样频率的设置
2.ADC数据传输模式改成DMA模式,可以进一步提高采样率
3.改成小电池供电
4加入DSP库实现 256点的DSP运算,计算出信号频率

(二)硬件介绍

(1)MCU

因为需要实现DSP,需要大量的数据空间,所以我将之前的C8T6改成了RCT6,两者在封装以及部件上没有什么明显的区别,只是FLASH从64K升级成256K,RAM从20K变成了48K,这样就有了更多的操作空间。

在这里插入图片描述

(2)TFT显示屏

再网上买的核心板存在一个直插的OLED接口,也可以作为TFT接口。

在这里插入图片描述

(3)按键接口

在这里插入图片描述

(三)驱动编程

(1)ADC驱动

在这里插入图片描述

因为只有ADC1可以使用DMA所以我们将PA0,PA1映射到ADC1 CH0和ADC CH1

为了将ADC的采样率我们需要将ADC设置为TIM溢出触发,我们将TIM3作为触发。同时将PCLK2 6分频,可以得到12M的ADCCLK。

转换时间计算,我们设置转换周期ADC_SampleTime_1Cycles5
可以计算最短转换时间为(1.5+12.5)/12M=1.17us
0.85M采样率左右
当然实际肯定是达不到的。

ADC.h

#ifndef ADC_H
#define ADC_H

#include "sys.h"
#define ADC_CHANNEL_NUMS         2
#define SAMPLS_NUM            1024

#define TIM3_DEFAULT 9
#define TIM2_PERIOD  999

extern u16 ADC_SourceData[SAMPLS_NUM][ADC_CHANNEL_NUMS];

//ADCCLK=12M
void Adc_Init(void);
void ADC_SetFreq(u32 FREQ);
u32 ADC_GetFREQ(void);
#endif

ADC.c

#include "sys.h"
#include "usart.h"
#include "ADC.h"

u16 ADC_SourceData[SAMPLS_NUM][ADC_CHANNEL_NUMS] = {0};
u32 ADC_FREQ=100000;

void ADC_GPIO_Configuration(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);       
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

void ADC_TIM2_GPIO_Configuration(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); 
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;   
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

void ADC_TIM2_Configuration(void)
{ 
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;
    ADC_TIM2_GPIO_Configuration();
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
    TIM_DeInit(TIM2);
    TIM_TimeBaseStructure.TIM_Period = TIM2_PERIOD - 1;
    TIM_TimeBaseStructure.TIM_Prescaler = 71;       
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, & TIM_TimeBaseStructure);
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;   
    TIM_OCInitStructure.TIM_Pulse = (TIM2_PERIOD - 1) / 2;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;        
    TIM_OC3Init(TIM2, & TIM_OCInitStructure);
}

void ADC_TIM3_Configuration(u16 TIM3_PERIOD)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; 
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
    TIM_TimeBaseInitStructure.TIM_Period = TIM3_PERIOD - 1;
    TIM_TimeBaseInitStructure.TIM_Prescaler = 71;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;         
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;     
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
    TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update);           
    TIM_Cmd(TIM3,ENABLE);                                   
}

void ADC_DMA_NVIC_Configuration(void)
{
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel  = DMA1_Channel1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    DMA_ClearITPendingBit(DMA1_IT_TC1);
    DMA_ITConfig(DMA1_Channel1,DMA1_IT_TC1,ENABLE);
}

void ADC_DMA_Configuration(void)
{
    DMA_InitTypeDef  DMA_InitStructure;
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
    DMA_DeInit(DMA1_Channel1);
    DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&ADC1->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr     = (u32)ADC_SourceData;
    DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize         = ADC_CHANNEL_NUMS*SAMPLS_NUM;
    DMA_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode               = DMA_Mode_Circular;
    DMA_InitStructure.DMA_Priority           = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M                = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);
    DMA_Cmd(DMA1_Channel1, ENABLE); 
    ADC_DMA_NVIC_Configuration();
}
//ADCCLK=12M
//ADC_SampleTime_28Cycles5 ת»»ÖÜÆÚ28.5+12.5=41¸öÖÜÆÚ
//ADC_SampleTime_1Cycles5  ת»»ÖÜÆÚ1 .5+12.5=14¸öÖÜÆÚ
//×î¶Ìת»»Ê±¼ä£º 14/12=1.17us
//²ÉÑùÂÊ×î¸ß0.85M
void ADC_Init_Configuration(void)
{
    ADC_InitTypeDef  ADC_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);//AFCCLK:72/6=12MHz
    ADC_DeInit(ADC1);
    ADC_InitStructure.ADC_Mode               = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode       = ENABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConv   = ADC_ExternalTrigConv_T3_TRGO;
    ADC_InitStructure.ADC_DataAlign          = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel       = ADC_CHANNEL_NUMS;
    ADC_Init(ADC1, &ADC_InitStructure);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0,  1, ADC_SampleTime_1Cycles5); //AI_VS_A1
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1,  2, ADC_SampleTime_1Cycles5); //AI_VS_B1

    ADC_DMACmd(ADC1, ENABLE);
    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));
    ADC_ExternalTrigConvCmd(ADC1, ENABLE);
}




void Adc_Init(void)
{
    ADC_GPIO_Configuration();
    ADC_TIM3_Configuration(TIM3_DEFAULT);
    ADC_DMA_Configuration();
    ADC_Init_Configuration();
}
u32 ADC_GetFREQ(void)
{
    return ADC_FREQ;
}
//TIM3¶¨Ê±Æ÷1M
//×î¸ß´¥·¢ PERIOD=0                 ²ÉÑùÂÊ=1M
//                 PERIOD=1               ²ÉÑùÂÊ=0.5M
//                 PERIOD=3               ²ÉÑùÂÊ=0.25M        250K
//                 PERIOD=3               ²ÉÑùÂÊ=0.1M         100K
//                 PERIOD=999           ²ÉÑùÂÊ=1K
//                 PERIOD=60000       ²ÉÑùÂÊ=16
void ADC_SetFreq(u32 FREQ)
{
    u16 PERIOD=((float)1000000/FREQ)-1;
    TIM3->ARR=PERIOD;
    ADC_FREQ=FREQ;
}

void DMA1_Channel1_IRQHandler(void)
{
    if(DMA_GetITStatus(DMA1_IT_TC1) != RESET)
    {
        DMA_ClearITPendingBit(DMA1_IT_TC1);
    }
}

我设置将 ADC数据的目标地址设置为 ADC_SourceData。

(2)LCD驱动

(3)一路测试PWM

#include "SERVO.h"
#include "delay.h"
void TIM4_PWM_Init(u16 arr,u16 psc)
{
    RCC->APB1ENR|=1<<2;       
    GPIOB->CRH&=0XFFFFFF00;
    GPIOB->CRH|=0X000000BB;  
    GPIOB->ODR|=3<<8;
    TIM4->ARR=arr;
    TIM4->PSC=psc;
    TIM4->CCMR2|=6<<4; 
    TIM4->CCMR2|=1<<3; 
    TIM4->CCMR2|=6<<12;  
    TIM4->CCMR2|=1<<11;  
    TIM4->CCER|=1<<8;  
    TIM4->CCER|=1<<12;  
    TIM4->CR1=0x0080;  
    TIM4->CR1|=0x01;   
}

void Servo_Init(void)
{
    TIM4_PWM_Init(20000,71);//50HZ
    TIM4->CCR3 =10000;
    TIM4->CCR4 =15000;
}
void SERVO1(u8 degree)
{
    u16 k;
    k = 500 + degree * 11;
    TIM4->CCR3 = k;
}
void SERVO2(u8 degree)
{
    u16 k;
    k = 500 + degree * 11;
    TIM4->CCR4 = k;
}

(四)FFT原理

采集出信号频率以后我们需要在短时间内计算出信号的频率。

通过一组离散信号计算信号频率的方法通常叫做离散傅里叶变换。

我们首先来看一下DFT的原理:

在这里插入图片描述

学过信号的都知道,信号从时域来看是不断变换的,同时也可以从一个完全不同的角度来看信号。
DFT(FFT)的作用:可以将信号从时域变换到频域,而且时域和频域都是离散的,通俗的说,可以求出一个信号由哪些正弦波叠加而成,求出的结果就是这些正弦波的幅度和相位,我们音乐播放器上面显示的就是音乐fft之后不同频率正弦波的幅度,就像下面这张图片:

在这里插入图片描述

DFT公式:
拆分以后可以得到

在这里插入图片描述

我们可以通过一组信号来测试:

DFT之后的数据是对称的,,在FFT的章节。比如做8点DFT,采样信号为x(n),DFT之后的数据为X(k),那么X(0)为直流信号,X(1), X(2), X(3), X(5), X(6), X(7),关于X(4)对称,即X(1)=X(7), X(2)=X(6),X(3)=X(5),如下图,是对1+sin(2PIt)进行DFT变换,具体的幅值先不关心,只要知道它是对称的就行了

在这里插入图片描述


我们可以看到得到的离散幅值序列,就是采样点数。这个时候点数的含义就从时间序列变成了频率序列。那么怎么从这个序列转换为我们需要的频率呢。

以上面的例子为例:

我们得到 0-7的离散序列可以对应到0-FS(采样频率)

具体公式为

我们在这个计算过程中可以看到采样频率一定要大于信号频率的两倍以上,否则幅值最高的频点就无法在你的采样范围内了。
我对DFT的理解其实就是将你的采样离散信号与采样信号进行卷积,这样以来,信号在频率中幅值最高的部分就会被突出出来。
FFT(Fast Fourier Transformation),中文名快速傅里叶变换,是离散傅氏变换的快速算法,它是根据离散傅氏变换的奇、偶、虚、实等特性,对离散傅立叶变换的算法进行改进获得的。
而在信奥中,一般用来加速多项式乘法。
朴素高精度乘法的时间为 O ( n 2 ) O(n^2) O(n2),但FFT能将时间复杂度降到 O ( n l o g 2 n ) O(nlog_2n) O(nlog2​n)

一下是我用C语言实现的简单FFT

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-04-06 08:17:24
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-04-06 11:02:16
 */
#include "stdio.h"
#include "math.h"
#include <time.h>
void kfft(double pr[],double pi[],int n,int k,double fr[],double fi[],int l,int il)
{ 
    int it,m,is,i,j,nv,l0;
    double p,q,s,vr,vi,poddr,poddi;
    for (it=0; it<=n-1; it++)
      { m=it; is=0;
        for (i=0; i<=k-1; i++)
          { j=m/2; is=2*is+(m-2*j); m=j;}
        fr[it]=pr[is]; fi[it]=pi[is];
      }
    pr[0]=1.0; pi[0]=0.0;
    p=6.283185306/(1.0*n);
    pr[1]=cos(p); pi[1]=-sin(p);
    if (l!=0) pi[1]=-pi[1];
    for (i=2; i<=n-1; i++)
      { p=pr[i-1]*pr[1]; q=pi[i-1]*pi[1];
        s=(pr[i-1]+pi[i-1])*(pr[1]+pi[1]);
        pr[i]=p-q; pi[i]=s-p-q;
      }
    for (it=0; it<=n-2; it=it+2)
      { vr=fr[it]; vi=fi[it];
        fr[it]=vr+fr[it+1]; fi[it]=vi+fi[it+1];
        fr[it+1]=vr-fr[it+1]; fi[it+1]=vi-fi[it+1];
      }
    m=n/2; nv=2;
    for (l0=k-2; l0>=0; l0--)
      { m=m/2; nv=2*nv;
        for (it=0; it<=(m-1)*nv; it=it+nv)
          for (j=0; j<=(nv/2)-1; j++)
            { p=pr[m*j]*fr[it+j+nv/2];
              q=pi[m*j]*fi[it+j+nv/2];
              s=pr[m*j]+pi[m*j];
              s=s*(fr[it+j+nv/2]+fi[it+j+nv/2]);
              poddr=p-q; poddi=s-p-q;
              fr[it+j+nv/2]=fr[it+j]-poddr;
              fi[it+j+nv/2]=fi[it+j]-poddi;
              fr[it+j]=fr[it+j]+poddr;
              fi[it+j]=fi[it+j]+poddi;
            }
      }
    if (l!=0)
      for (i=0; i<=n-1; i++)
        { fr[i]=fr[i]/(1.0*n);
          fi[i]=fi[i]/(1.0*n);
        }
    if (il!=0)
      for (i=0; i<=n-1; i++)
        { pr[i]=sqrt(fr[i]*fr[i]+fi[i]*fi[i]);
          if (fabs(fr[i])<0.000001*fabs(fi[i]))
            { if ((fi[i]*fr[i])>0) pi[i]=90.0;
              else pi[i]=-90.0;
            }
          else
            pi[i]=atan(fi[i]/fr[i])*360.0/6.283185306;
        }
    return;
}



#define PI 3.1415926535
#define FS 128
#define N (512)
#define M (int)log2(N)
 int main()
  { 
      int i;
      double t;
      double pr[N],pi[N],fr[N],fi[N];
    clock_t start,end;
      //产生抽样序列
      for (i=0; i<N; i++)
      { 
         t=(double)i/FS;
         pr[i]=sin(2*PI*33*t);
         pi[i]=0.0;
      }
    start=clock();
      kfft(pr,pi,N,M,fr,fi,0,1);  //调用fft函数
    end=clock();
      for (i=0; i<N; i++)
      {
          printf("%.6f %.6f\n",FS*(i+1)/(double)N,pr[i]); //输出数据
        }
      printf("time:%d\n",(end-start));

      return 0;
  }

下面是输出序列
0.250000 0.000000
0.500000 0.000000
0.750000 0.000000
1.000000 0.000000
1.250000 0.000000
1.500000 0.000000
1.750000 0.000000
2.000000 0.000000
2.250000 0.000000
2.500000 0.000000
2.750000 0.000000
3.000000 0.000000
3.250000 0.000000
3.500000 0.000000
3.750000 0.000000
4.000000 0.000000
4.250000 0.000000
4.500000 0.000000
4.750000 0.000000
5.000000 0.000000
5.250000 0.000000
5.500000 0.000000
5.750000 0.000000
6.000000 0.000000
6.250000 0.000000
6.500000 0.000000
6.750000 0.000000
7.000000 0.000000
7.250000 0.000000
7.500000 0.000000
7.750000 0.000000
8.000000 0.000000
8.250000 0.000000
8.500000 0.000000
8.750000 0.000000
9.000000 0.000000
9.250000 0.000000
9.500000 0.000000
9.750000 0.000000
10.000000 0.000000
10.250000 0.000000
10.500000 0.000000
10.750000 0.000000
11.000000 0.000000
11.250000 0.000000
11.500000 0.000000
11.750000 0.000000
12.000000 0.000000
12.250000 0.000000
12.500000 0.000000
12.750000 0.000000
13.000000 0.000000
13.250000 0.000000
13.500000 0.000000
13.750000 0.000000
14.000000 0.000000
14.250000 0.000000
14.500000 0.000000
14.750000 0.000000
15.000000 0.000000
15.250000 0.000000
15.500000 0.000000
15.750000 0.000000
16.000000 0.000000
16.250000 0.000000
16.500000 0.000000
16.750000 0.000000
17.000000 0.000000
17.250000 0.000000
17.500000 0.000000
17.750000 0.000000
18.000000 0.000000
18.250000 0.000000
18.500000 0.000000
18.750000 0.000000
19.000000 0.000000
19.250000 0.000000
19.500000 0.000000
19.750000 0.000000
20.000000 0.000000
20.250000 0.000000
20.500000 0.000000
20.750000 0.000000
21.000000 0.000000
21.250000 0.000000
21.500000 0.000000
21.750000 0.000000
22.000000 0.000000
22.250000 0.000000
22.500000 0.000000
22.750000 0.000000
23.000000 0.000000
23.250000 0.000000
23.500000 0.000000
23.750000 0.000000
24.000000 0.000000
24.250000 0.000000
24.500000 0.000000
24.750000 0.000000
25.000000 0.000000
25.250000 0.000000
25.500000 0.000000
25.750000 0.000000
26.000000 0.000000
26.250000 0.000000
26.500000 0.000000
26.750000 0.000000
27.000000 0.000000
27.250000 0.000000
27.500000 0.000000
27.750000 0.000000
28.000000 0.000000
28.250000 0.000000
28.500000 0.000000
28.750000 0.000000
29.000000 0.000000
29.250000 0.000000
29.500000 0.000000
29.750000 0.000000
30.000000 0.000000
30.250000 0.000000
30.500000 0.000000
30.750000 0.000000
31.000000 0.000000
31.250000 0.000000
31.500000 0.000000
31.750000 0.000000
32.000000 0.000000
32.250000 0.000000
32.500000 0.000000
32.750000 0.000000
33.000000 0.000001
33.250000 256.000000
33.500000 0.000001
33.750000 0.000000
34.000000 0.000000
34.250000 0.000000
34.500000 0.000000
34.750000 0.000000
35.000000 0.000000
35.250000 0.000000
35.500000 0.000000
35.750000 0.000000
36.000000 0.000000
36.250000 0.000000
36.500000 0.000000
36.750000 0.000000
37.000000 0.000000
37.250000 0.000000
37.500000 0.000000
37.750000 0.000000
38.000000 0.000000
38.250000 0.000000
38.500000 0.000000
38.750000 0.000000
39.000000 0.000000
39.250000 0.000000
39.500000 0.000000
39.750000 0.000000
40.000000 0.000000
40.250000 0.000000
40.500000 0.000000
40.750000 0.000000
41.000000 0.000000
41.250000 0.000000
41.500000 0.000000
41.750000 0.000000
42.000000 0.000000
42.250000 0.000000
42.500000 0.000000
42.750000 0.000000
43.000000 0.000000
43.250000 0.000000
43.500000 0.000000
43.750000 0.000000
44.000000 0.000000
44.250000 0.000000
44.500000 0.000000
44.750000 0.000000
45.000000 0.000000
45.250000 0.000000
45.500000 0.000000
45.750000 0.000000
46.000000 0.000000
46.250000 0.000000
46.500000 0.000000
46.750000 0.000000
47.000000 0.000000
47.250000 0.000000
47.500000 0.000000
47.750000 0.000000
48.000000 0.000000
48.250000 0.000000
48.500000 0.000000
48.750000 0.000000
49.000000 0.000000
49.250000 0.000000
49.500000 0.000000
49.750000 0.000000
50.000000 0.000000
50.250000 0.000000
50.500000 0.000000
50.750000 0.000000
51.000000 0.000000
51.250000 0.000000
51.500000 0.000000
51.750000 0.000000
52.000000 0.000000
52.250000 0.000000
52.500000 0.000000
52.750000 0.000000
53.000000 0.000000
53.250000 0.000000
53.500000 0.000000
53.750000 0.000000
54.000000 0.000000
54.250000 0.000000
54.500000 0.000000
54.750000 0.000000
55.000000 0.000000
55.250000 0.000000
55.500000 0.000000
55.750000 0.000000
56.000000 0.000000
56.250000 0.000000
56.500000 0.000000
56.750000 0.000000
57.000000 0.000000
57.250000 0.000000
57.500000 0.000000
57.750000 0.000000
58.000000 0.000000
58.250000 0.000000
58.500000 0.000000
58.750000 0.000000
59.000000 0.000000
59.250000 0.000000
59.500000 0.000000
59.750000 0.000000
60.000000 0.000000
60.250000 0.000000
60.500000 0.000000
60.750000 0.000000
61.000000 0.000000
61.250000 0.000000
61.500000 0.000000
61.750000 0.000000
62.000000 0.000000
62.250000 0.000000
62.500000 0.000000
62.750000 0.000000
63.000000 0.000000
63.250000 0.000000
63.500000 0.000000
63.750000 0.000000
64.000000 0.000000
64.250000 0.000000
64.500000 0.000000
64.750000 0.000000
65.000000 0.000000
65.250000 0.000000
65.500000 0.000000
65.750000 0.000000
66.000000 0.000000
66.250000 0.000000
66.500000 0.000000
66.750000 0.000000
67.000000 0.000000
67.250000 0.000000
67.500000 0.000000
67.750000 0.000000
68.000000 0.000000
68.250000 0.000000
68.500000 0.000000
68.750000 0.000000
69.000000 0.000000
69.250000 0.000000
69.500000 0.000000
69.750000 0.000000
70.000000 0.000000
70.250000 0.000000
70.500000 0.000000
70.750000 0.000000
71.000000 0.000000
71.250000 0.000000
71.500000 0.000000
71.750000 0.000000
72.000000 0.000000
72.250000 0.000000
72.500000 0.000000
72.750000 0.000000
73.000000 0.000000
73.250000 0.000000
73.500000 0.000000
73.750000 0.000000
74.000000 0.000000
74.250000 0.000000
74.500000 0.000000
74.750000 0.000000
75.000000 0.000000
75.250000 0.000000
75.500000 0.000000
75.750000 0.000000
76.000000 0.000000
76.250000 0.000000
76.500000 0.000000
76.750000 0.000000
77.000000 0.000000
77.250000 0.000000
77.500000 0.000000
77.750000 0.000000
78.000000 0.000000
78.250000 0.000000
78.500000 0.000000
78.750000 0.000000
79.000000 0.000000
79.250000 0.000000
79.500000 0.000000
79.750000 0.000000
80.000000 0.000000
80.250000 0.000000
80.500000 0.000000
80.750000 0.000000
81.000000 0.000000
81.250000 0.000000
81.500000 0.000000
81.750000 0.000000
82.000000 0.000000
82.250000 0.000000
82.500000 0.000000
82.750000 0.000000
83.000000 0.000000
83.250000 0.000000
83.500000 0.000000
83.750000 0.000000
84.000000 0.000000
84.250000 0.000000
84.500000 0.000000
84.750000 0.000000
85.000000 0.000000
85.250000 0.000000
85.500000 0.000000
85.750000 0.000000
86.000000 0.000000
86.250000 0.000000
86.500000 0.000000
86.750000 0.000000
87.000000 0.000000
87.250000 0.000000
87.500000 0.000000
87.750000 0.000000
88.000000 0.000000
88.250000 0.000000
88.500000 0.000000
88.750000 0.000000
89.000000 0.000000
89.250000 0.000000
89.500000 0.000000
89.750000 0.000000
90.000000 0.000000
90.250000 0.000000
90.500000 0.000000
90.750000 0.000000
91.000000 0.000000
91.250000 0.000000
91.500000 0.000000
91.750000 0.000000
92.000000 0.000000
92.250000 0.000000
92.500000 0.000000
92.750000 0.000000
93.000000 0.000000
93.250000 0.000000
93.500000 0.000000
93.750000 0.000000
94.000000 0.000000
94.250000 0.000000
94.500000 0.000000
94.750000 0.000000
95.000000 0.000001
95.250000 256.000000
95.500000 0.000001
95.750000 0.000000
96.000000 0.000000
96.250000 0.000000
96.500000 0.000000
96.750000 0.000000
97.000000 0.000000
97.250000 0.000000
97.500000 0.000000
97.750000 0.000000
98.000000 0.000000
98.250000 0.000000
98.500000 0.000000
98.750000 0.000000
99.000000 0.000000
99.250000 0.000000
99.500000 0.000000
99.750000 0.000000
100.000000 0.000000
100.250000 0.000000
100.500000 0.000000
100.750000 0.000000
101.000000 0.000000
101.250000 0.000000
101.500000 0.000000
101.750000 0.000000
102.000000 0.000000
102.250000 0.000000
102.500000 0.000000
102.750000 0.000000
103.000000 0.000000
103.250000 0.000000
103.500000 0.000000
103.750000 0.000000
104.000000 0.000000
104.250000 0.000000
104.500000 0.000000
104.750000 0.000000
105.000000 0.000000
105.250000 0.000000
105.500000 0.000000
105.750000 0.000000
106.000000 0.000000
106.250000 0.000000
106.500000 0.000000
106.750000 0.000000
107.000000 0.000000
107.250000 0.000000
107.500000 0.000000
107.750000 0.000000
108.000000 0.000000
108.250000 0.000000
108.500000 0.000000
108.750000 0.000000
109.000000 0.000000
109.250000 0.000000
109.500000 0.000000
109.750000 0.000000
110.000000 0.000000
110.250000 0.000000
110.500000 0.000000
110.750000 0.000000
111.000000 0.000000
111.250000 0.000000
111.500000 0.000000
111.750000 0.000000
112.000000 0.000000
112.250000 0.000000
112.500000 0.000000
112.750000 0.000000
113.000000 0.000000
113.250000 0.000000
113.500000 0.000000
113.750000 0.000000
114.000000 0.000000
114.250000 0.000000
114.500000 0.000000
114.750000 0.000000
115.000000 0.000000
115.250000 0.000000
115.500000 0.000000
115.750000 0.000000
116.000000 0.000000
116.250000 0.000000
116.500000 0.000000
116.750000 0.000000
117.000000 0.000000
117.250000 0.000000
117.500000 0.000000
117.750000 0.000000
118.000000 0.000000
118.250000 0.000000
118.500000 0.000000
118.750000 0.000000
119.000000 0.000000
119.250000 0.000000
119.500000 0.000000
119.750000 0.000000
120.000000 0.000000
120.250000 0.000000
120.500000 0.000000
120.750000 0.000000
121.000000 0.000000
121.250000 0.000000
121.500000 0.000000
121.750000 0.000000
122.000000 0.000000
122.250000 0.000000
122.500000 0.000000
122.750000 0.000000
123.000000 0.000000
123.250000 0.000000
123.500000 0.000000
123.750000 0.000000
124.000000 0.000000
124.250000 0.000000
124.500000 0.000000
124.750000 0.000000
125.000000 0.000000
125.250000 0.000000
125.500000 0.000000
125.750000 0.000000
126.000000 0.000000
126.250000 0.000000
126.500000 0.000000
126.750000 0.000000
127.000000 0.000000
127.250000 0.000000
127.500000 0.000000
127.750000 0.000000
128.000000 0.000000

在频点33的位置幅值最高:

1* (FS)/N
那么怎样将256采样点转换为采样频率呢
我们可以通过以下函数转化频率:

float Get_Signal_Freq(int index)
{
    InitBuflnArray(index);
    DSP();
    return ((float)GetMaxData_Index()/256)*ADC_GetFREQ();
}

MyDSP.c

#include "MyDSP.h"
#include "math.h"
#include "ADC.h"

long     FFT_256Pointln[N];
long     FFT_256PointOut[N/2];
float IBufMagArray[N/2];
float Fs = 44800;    
float F = 44800/N;

void InitBuflnArray(int index)
{
    unsigned short i;
    float fx;
    Fs=ADC_GetFREQ();
    F = Fs/N;
    for(i = 0; i < N; i++)
    {
        fx = ADC_SourceData[i][index];
        FFT_256Pointln[i] = ((signed short)fx) << 16;   
    }
}

void GetPowerMag(void)
{
    signed short IX, IY;
    float X, Y, Mag;
    unsigned short i;
    for(i = 0; i < N/2;i++)
    {
        IX = (FFT_256PointOut[i] << 16) >> 16;  
        IY = (FFT_256PointOut[i] >> 16);                
        X = N * ((float)IX) / 32768;
        Y = N * ((float)IY) / 32768;
        Mag = sqrt(X*X + Y*Y) / N;
        if(i == 0)
        {
            IBufMagArray[i] = (Mag * 32768);
        }
        else
        {
            IBufMagArray[i] = (Mag * 65536);
        }
    }
}


float GetMaxData(void)    
{
    int i = 0;
    float temp = 0;
    for(i = 1; i < N/2; i++)
    {
        if(temp < IBufMagArray[i])
        {
            temp = IBufMagArray[i];
        }
    }
    return temp;
}

int GetMaxData_Index(void)    
{
    int i = 0;
    float temp = 0;
    int tempindex=0;
    for(i = 1; i < N/2; i++)
    {
        if(temp < IBufMagArray[i])
        {
            temp = IBufMagArray[i];
            tempindex=i;
        }
    }
    return tempindex;
}


void DSP(void)    
{
    cr4_fft_256_stm32(FFT_256PointOut, FFT_256Pointln, N);
    GetPowerMag();
}

int Get_Adc_Max_Min(int index)
{
    int i = 0;
    int max = 0,min=0;
    if(index>1)index=0;
    for(i = 0; i < SAMPLS_NUM; i++)
    {
        if(max < ADC_SourceData[i][index])
            max = ADC_SourceData[i][index];
        if(min > ADC_SourceData[i][index])
            min = ADC_SourceData[i][index];
    }
    return max-min;
}

float Get_Signal_Freq(int index)
{
    InitBuflnArray(index);
    DSP();
    return ((float)GetMaxData_Index()/256)*ADC_GetFREQ();
}

(四)操作逻辑

main.c

#include "led.h"
#include "delay.h"
#include "sys.h"
#include "usart.h"
#include "KEY.h"
#include "LCD_X.h"
#include "W25X16.h"
#include "malloc.h"
#include "SERVO.h"
#include "ADC.h"
#include "MyDSP.h"
u8 CHKEY=0; //0 CH0 1 CH1 2 

void DSO_Win()
{
    u8 data[3];
    int t=0;
    POINT_COLOR=GRAY;
    LCD_DrawFillRectangle(0,0,128,116);

    POINT_COLOR=BLUE;
    LCD_DrawFillRectangle(0,116,55,128);
    LCD_Show_Str(0,116,RED,BLUE,(u8*)"1HZ1V",12,1);

    POINT_COLOR=YELLOW;
    LCD_DrawFillRectangle(55,116,110,128);
    LCD_ShowString(55,116,12,(u8*)"1HZ1V",1);
    POINT_COLOR=GREEN;
    LCD_DrawFillRectangle(110,116,128,128);
    t=ADC_GetFREQ();
    sprintf((char*)data,"%3.1fK",(float)t/1000);
    LCD_ShowString(110,116,12,(u8*)data,1);
}

void CH1_Update(u32 freq,float vos)
{
    u8 data[10]={0};
    POINT_COLOR=BLUE;
    LCD_DrawFillRectangle(0,116,55,128);
    sprintf((char*)data,"%dHZ%1.1fV",freq,vos);
    LCD_Show_Str(0,116,RED,BLUE,(u8*)data,12,1);
}

void CH2_Update(int freq,float vos)
{
    u8 data[10]={0};
    POINT_COLOR=YELLOW;
    LCD_DrawFillRectangle(55,116,110,128);
    sprintf((char*)data,"%dHZ%1.1fV",freq,vos);
    LCD_Show_Str(55,116,RED,BLUE,(u8*)data,12,1);
}

void FREQ_Update(int freq)
{
    u8 data[3]={0};
    POINT_COLOR=GREEN;
    LCD_DrawFillRectangle(110,116,128,128);
    sprintf((char*)data,"%3.1f",(float)freq/1000);
    LCD_Show_Str(110,116,RED,BLUE,(u8*)data,12,1);
}
 int main(void)
 {    
    int i;
    u8 key;
    u32 freq1,freq2;
    float vos1,vos2;
    u32 freq;
    u8 line0[128];
    u8 line1[128];
    delay_init();            
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);      
    uart_init(115200); 
    LED_Init();         
    KEY_Init();
    LCD_Init();
    LCD_Clear(WHITE);
    SPI_Flash_Init();
    Adc_Init();
    ADC_SetFreq(100);
    Servo_Init();
    DSO_Win();

    while(1){
         key=KEY_Scan(0);
         if(key==KEY0_PRES){
                CHKEY++;
                if(CHKEY>=3)CHKEY=0;
         }
         if(key==KEY1_PRES){
                freq=ADC_GetFREQ();
              freq*=2;//²»Äܳ¬¹ý500K
              if(freq>=500000)freq=100;
              ADC_SetFreq(freq);
                FREQ_Update(freq);
         }
            POINT_COLOR=GRAY;
            LCD_DrawFillRectangle(0,0,128,116);
        if(CHKEY==0){
            for(i=1;i<128;i++){
                line0[i]=116-ADC_SourceData[i][0]/37;
                LCD_DrawLine_Color(128-(i-1),line0[i-1],128-i,line0[i],BLUE);
            }
        }
        else if(CHKEY==1){
            for(i=1;i<128;i++){
                line1[i]=116-ADC_SourceData[i][1]/37;
                LCD_DrawLine_Color(128-(i-1),line1[i-1],128-i,line1[i],YELLOW);
            }       
        }
        else
        {
            for(i=1;i<128;i++){
                line0[i]=116-ADC_SourceData[i][0]/37;
                LCD_DrawLine_Color(128-(i-1),line0[i-1],128-i,line0[i],BLUE);
            }
            for(i=1;i<128;i++){
                line1[i]=116-ADC_SourceData[i][1]/37;
                LCD_DrawLine_Color(128-(i-1),line1[i-1],128-i,line1[i],YELLOW);
            }   
        }
        freq1=Get_Signal_Freq(0);
        freq2=Get_Signal_Freq(1);
        vos1=3.3*(float)Get_Adc_Max_Min(0)/4096;
        vos2=3.3*(float)Get_Adc_Max_Min(1)/4096;
        CH1_Update(freq1,vos1);
        CH2_Update(freq2,vos2);
    }
}

可以通过KEY1更改采样率
KEY0更改通道

(五)操作效果

采样50HZ PWM

0.1KHz

在这里插入图片描述

0.2KHz

在这里插入图片描述

0.4KHZ

在这里插入图片描述

0.8KHz

在这里插入图片描述

1.6KHz

在这里插入图片描述

3.2K

在这里插入图片描述

6.4KHz

在这里插入图片描述

双通道

在这里插入图片描述


经过在保证对于信号频率的测量准确的前提下,最高采样频率为500K。

(六)下载地址

下载地址

【机器人原理与实践(三)】六轴机械臂正逆解控制

@[toc]
本章针对机械臂的运动学进行建模分析。机械臂运动学模型反映的是个关节的角度与执行器末端位姿之间的映射关系。在工业过程中需要考虑的根据期望位姿推导关节角度(机械臂逆解),以及根据现有的关节角度推导末端执行器位姿(机械臂正解)。

3.1 空间转换矩阵的理解

3.1.1平移变换

在这里插入图片描述
在这里插入图片描述

3.1.2旋转变换

在这里插入图片描述

除了位置之外,还需要对刚体的指向,即姿态进行描述。而这也是旋转矩阵最本质的来源,即来源于 坐标系的旋转,这一部分理解不好或是理解不透彻,会导致此后面对各种形式的旋转时出现混乱,所以公式将进行详细的表述。

描述姿态的方式就是为刚体建立一个固连于刚体的标准正交坐标系,并由 其相对于参考坐标系的 单位向量 在参考坐标系中的描述 来表示。如上图所示,刚体的局部固连参考系为o-xyz,其局部固连参考系坐标系的单位向量为(x,y,z),而这局部坐标系的单位向量在参考坐标系 O−xyz 中的表示为:

在这里插入图片描述

可以进行进一步的整理,即上式等价于:

在这里插入图片描述

最后一个矩阵就是两个坐标系基之间的关系,即两个坐标系之间的转换关系,将这个矩阵定义为旋转矩阵

在这里插入图片描述

结合上面两种变换可以得到三维空间变换的一般形式:

在这里插入图片描述

用这个4X4的矩阵就可以描述四个机械臂末端相对于机械臂位置的变换,而当你将机械臂的起始坐标设置为(0,0,0)你就可以直接通过上述矩阵确定机械臂的末端位姿。(px,py,pz)将直接反应机械臂末端所直接对应的坐标。而n,o,a将直接反应机械臂末端坐标系相对于机械臂基座坐标系所发生的的旋转。

3.2 D-H参数法

D-H参数法是Denavit和Hartenberg在他们1955年提出的一种机器人的建模方法。该方法看似简单,但是通用性很强,虽然年代久远,但仍然是推导机器人运动学方程的标准方法。
机械臂通常有多个关节组成。D-H参数法在这些关节处以一定的规则建立参考,然后找到两两相邻的关节坐标系之间的转换关系,就是说在建立了坐标系的约束下,有限次的平移的旋转运动可以使用一个齐次矩阵表示这种转换关系。只要将相应的转换矩阵相乘,就可以得到任意两个关节坐标系的位置关系。在实际应用中,仅仅需要第一关节与最后一个关节的转换关系,就可在首关节确定的情况下确定末端执行器位姿。

在这里插入图片描述

以上图为例从关节n+1到关节n+2:
绕z轴转θ(n+1),使x_n轴和x(n+1)轴相互平行。
延z轴平移d_(n+1),使x_n轴和x_(n+1)轴共线。
延x轴平移a_(n+1),使x_n轴和x_(n+1)轴原点重合。
z_n轴绕x_(n+1)轴旋转旋转α(n+1)角度,使z_n z(n+1)重合。
相邻坐标系之间的变换都可以遵循以上步骤:
绕z轴转θ_(n+1):

在这里插入图片描述

延z轴平移d_(n+1):

在这里插入图片描述

延x轴平移a_(n+1):

在这里插入图片描述

z_n轴绕x_(n+1)轴旋转旋转α_(n+1)角度:

在这里插入图片描述

总变换矩阵为以上矩阵按顺序相乘

在这里插入图片描述

这就是任意相邻坐标系之间转换矩阵的通式。根据上述描述可以很容易的发现,使用D-H参数法建立机械臂运动学方程,关键在于要根据具体的机械臂建立合适点的关节坐标系。建立这样的一个关节坐标系需要四个关键参数,即θ,d,a,α, 这些参数就是机械臂的D-H参数,我们也可以根据这些参数使用Matlab进行相关的仿真。

3.3 建立机械臂模型

3.3.1 机械臂模型介绍

在这里插入图片描述
在这里插入图片描述


这是一个简单的六轴机械臂

图3.5 简化图片

经过测量以后得到机械臂的D-H参数表:

表格 3.1 D-H参数表

序号θdaα
0-1θ1580pi/2
1-2θ2+pi/20550
2-3θ30550
3-4θ40550
使用Matlab 机器人工具箱构建仿真
L(1) = Link([ 0, d(1), aa(1), alpha(1),0,offset(1) ]);
L(2) = Link([ 0, d(2), aa(2), alpha(2) ,0,offset(2) ]);
L(3) = Link([ 0, d(3), aa(3), alpha(3),0,offset(3) ]);
L(4) = Link([ 0, d(4), aa(4), alpha(4),0,offset(4) ]);

结合表3.1与公式2.4结合Matlab得到A1-A6所有的转换矩阵,与总的转换矩阵:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

那么该机械臂的正运动学方程即为以上4个矩阵按顺序乘积,由于结构太过于复杂这里不直接列出了A04=A01A12A23*A34;

在得到旋转矩阵之后,根据以下公式就可求出末端执行器的位姿

这里有四个旋转角,在D-H参数已知的情况下,该运动学方程的计算结果就是机械臂末端相对机械臂底端坐标系的位姿矩阵。

3.3.2 使用Matlab进行示教仿真

在这里插入图片描述

3.4 机器人运动学

3.4.1 机器人正运动学

关于机械臂的正运动学问题,可以调用Robotics工具箱中的fkine函数进行求解。函数调用的一般形式为T=R.fkine{Q},其中R表示机械臂模型,T为前向运动的正解,Q为机械臂四个关节的角度值。
取Q1=[pi/5,-pi/3.pi/2.-pi/4],带入fkine函数,或者直接带入公式1计算的矩阵的到一个齐次变换矩阵如下:
T =

      -1         0        0       -130
     0         0        -1         0
     0        -1         0       113
     0         0         0         1

可以得到仿真图像:

在这里插入图片描述


根据得到的图像结合实际可以确定,我们做出的正向运动学仿真大致是正确的。

3.4.2机器人逆运动学

[4]我们已经看到了前向运动学问题。逆运动学问题更有趣,其解决方案更有用。在位置层面,问题表述为:“考虑到机器人手部的理想位置,所有机器人关节的角度必须是什么?”。人类一直在解决这个问题,甚至没有想到它。当你早上吃麦片时,你只需伸手拿起勺子。你不会想,“我的肩膀需要这样做,我的肘部需要这样做,等等。” 下面我们将看看大多数机器人如何解决这个问题。我们将从一个非常简单的例子开始:
将机械臂的末端指向(-130,0,113)
对于机器人逆运动学的问题同样可以使用工具箱中的ikine函数解决这个问题。
可以在指定转化函数p的情况下输入,得到所有关节的角度向量:
q=bot.ikine(p,’mask’,[1 1 1 1 0 0])
figure(2);
bot.plot(q)

p =

     -1         0         0      -130
     0         0        -1         0
     0        -1         0       113
     0         0         0         1

q =
0.0000 0.1111 1.0963 0.6225

在这里插入图片描述

当然只用MATLAB是没有用的我们必须使用可以在开发板上运行的语言重新实现这样的效果。
这样的逆解问题本质上其实是解方程的问题,我们可以将上面的问题转换为:
我们需要达到的效果是已知x,y,z(机械臂末端坐标),我们需要计算出th1,th2,th3,th4
通过三个算式四个未知数显然是不太可能的,所以可以指定出th4也就是末端角度以后解算下面的方程。

eq1=75*sin(th4)*(cos(th1)*sin(th2)*sin(th3)-cos(th1)*cos(th2)*cos(th3)) -75*cos(th4)*(cos(th1)*cos(th2)*sin(th3) + cos(th1)*cos(th3)*sin(th2)) - 55*cos(th1)*sin(th2) - 55*cos(th1)*cos(th2)*sin(th3) - 55*cos(th1)*cos(th3)*sin(th2)==x;

eq2= 75*sin(th4)*(sin(th1)*sin(th2)*sin(th3) - cos(th2)*cos(th3)*sin(th1)) - 75*cos(th4)*(cos(th2)*sin(th1)*sin(th3) + cos(th3)*sin(th1)*sin(th2)) - 55*sin(th1)*sin(th2) - 55*cos(th2)*sin(th1)*sin(th3) - 55*cos(th3)*sin(th1)*sin(th2)==y;

eq3= 55*cos(th2) + 55*cos(th2)*cos(th3) - 55*sin(th2)*sin(th3) + 75*cos(th4)*(cos(th2)*cos(th3) - sin(th2)*sin(th3)) - 75*sin(th4)*(cos(th2)*sin(th3) + cos(th3)*sin(th2)) + 58==z;

[th1,th2,th3]

= solve(eq1,eq2,eq3,th1,th2,th3);

基于这样的方程解算通过python实现:

def changeto(x,y,z,th4,pwm):
    th4=th4*3.14/180
    def solve_function(unsolved_value):
        th1,th2,th3=unsolved_value[0],unsolved_value[1],unsolved_value[2]
        return [
            75*sin(th4)*(cos(th1)*sin(th2)*sin(th3) - cos(th1)*cos(th2)*cos(th3)) - 75*cos(th4)*(cos(th1)*cos(th2)*sin(th3) + cos(th1)*cos(th3)*sin(th2)) - 55*cos(th1)*sin(th2) - 55*cos(th1)*cos(th2)*sin(th3) - 55*cos(th1)*cos(th3)*sin(th2)-x,
            75*sin(th4)*(sin(th1)*sin(th2)*sin(th3) - cos(th2)*cos(th3)*sin(th1)) - 75*cos(th4)*(cos(th2)*sin(th1)*sin(th3) + cos(th3)*sin(th1)*sin(th2)) - 55*sin(th1)*sin(th2) - 55*cos(th2)*sin(th1)*sin(th3) - 55*cos(th3)*sin(th1)*sin(th2)-y,
            55*cos(th2) + 55*cos(th2)*cos(th3) - 55*sin(th2)*sin(th3) + 75*cos(th4)*(cos(th2)*cos(th3) - sin(th2)*sin(th3)) - 75*sin(th4)*(cos(th2)*sin(th3) + cos(th3)*sin(th2)) + 58-z,
        ]
    solved=fsolve(solve_function,[0, 0, 0])
    solved=solved*180/3.14
print(solved)

3.5 机械臂运动过程分析

机械臂的运动过程,也就是舵机驱动关节转动的过程。过程如下:

(1)通过程序设置初始化各个脱机状态。
(2)各个舵机转动一定角度,使得长臂、断臂和机械臂末端都处于水平位置,整个过程中 号舵机旋转一定角度控制机械爪处于打开状态。
(3)2号舵机旋转一定角度,使长臂处于垂直状态,同时机械爪关闭。
(4)3号关节旋转一定角度,使断臂穿衣水平状态,通过机械爪打开。
(5)关闭机械爪。
(6)使机械臂回到初始状态

在这里插入图片描述

3.6 本章小结

本章首先进行连杆坐标,使用并分析了 D-H 模型,并使用 MatLab 软件进行了正反解的验证,分析了机械臂运动学正逆解,并给出了仿真图。通过Matlab计算出的算式直接应用到python中,给出了在python中的正逆解算法。

【机器人原理与实践(二)】单目摄像头标定与单目测距

@[toc]

摄像头标定

我们可以想到在单目测量中我们在图像中找到某个目标以后可以获得的是一组像素的坐标,对于这样的一组像素并没有与实际空间中的转换关系,所以我们使用视觉传感的第一步就是尝试进行视觉标定,将图像中的坐标转换为实际的坐标。

摄像机标定(Camera calibration)简单来说是从世界坐标系转换为相机坐标系,再由相机坐标系转换为图像坐标系的过程,也就是求最终的投影矩阵的过程。即:世界坐标系 –> 相机坐标系 –> 图像坐标系 –> 像素坐标系
世界坐标系(world coordinate system):用户定义的三维世界的坐标系,为了描述目标物在真实世界里的位置而被引入,单位为m。
相机坐标系(camera coordinate system):在相机上建立的坐标系,为了从相机的角度描述物体位置而定义,作为连通世界坐标系和图像/像素坐标系的中间一环。单位为m。
图像坐标系(image coordinate system):为了描述成像过程中物体从相机坐标系到图像坐标系的投影透射关系而引入,方便进一步得到像素坐标系下的坐标,单位为m。
像素坐标系(pixel coordinate system):为了描述物体成像后的像点在数字图像上(相片)的坐标而引入,是我们真正从相机内读取到的信息所在的坐标系。单位为个(像素数目)。

4.1 单目相机的建模

4.1.1 图像坐标系到像素坐标系的转换

像素坐标系和图像坐标系都在成像平面上,只是各自的原点和度量单位不一样。图像坐标系的原点为相机光轴与成像平面的交点,通常情况下是成像平面的中点。图像坐标系的单位为mm,属于物理单位,而像素坐标系的单位是pixel,我们平常描述一个像素点都是几行几列。所以这两者之间的转换如下:其中dx和dy表示每一列和每一行分别代表多少mm,点(u,v)为像素坐标系上的点,对应的图像坐标系上的点为(x,y)。

在这里插入图片描述
在这里插入图片描述

4.1.2相机坐标系到图像坐标系的转换

设内参矩阵 K,相机坐标系 Q(Xc,Yc,Zc),投影图像坐标系 q ( x , y )。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.1.3 合并矩阵

在这里插入图片描述
在这里插入图片描述

分析这个公式,我们发现当我们知道世界坐标求解这个坐标在图片上的位置是一定可以求解出来的,但是当你获取到了图像上的坐标要求解这个位置在实际的坐标的时候,就会求不出来。这个就是没有深度信息的原因。这个时候我们需要建立一个模型,基于摄像头在显示中的位置,和主要的成像位置,可以直接通过像素算出距离信息。

4.2 固定向下摄像头标定

在这里插入图片描述

从上面的计算公式我们不难发现,在已知u,v的情况下,我们实际上是无法得出所在的位置相对于摄像头的坐标的,这是因为我们在某些情况下是必较难以获取所在位置的深度。简单的说就是单独的一个摄像头无法在不结合外部信息的情况下直接获取三维信息。在这种情况下可以有一种较为简单的方法,就是固定Z信息,就是对于图像上每一个点所有对应的Z都是固定的并且是已知的。

在这里插入图片描述
在这里插入图片描述


固定向下的矩阵计算是基于相机标定的一种特殊情况:

下面演示通过九点标定法,获取相机的矩阵:
1.准备好一张标定板,里面有3X3的圆。

在这里插入图片描述

2.对于所有的中心点记录所有的像素坐标和实际坐标
[-190,70,1],
[-180,10,1],
[-180,-70,1],
[-140,80,1],
[-140,10,1],
[-150,-140,1]]

[[324,331,1],
[578,296,1],
[958,331,1],
[210,115,1],
[587,133,1],
[1096,132,1]]
3.通过求解矩阵方程,获取转化矩阵
正常求解的话只需要三组点,也可以使用SVD分解法,这个时候需要六个点
采用SVD分解法求解矩阵方程

在这里插入图片描述

res,X=cv2.solve(A, B,flags= cv2.SVD_FULL_UV)

在这种情况下可以得到一个3X3的转换矩阵,基于这样的一个矩阵,在已知u,v像素坐标
的情况下可以计算出世界坐标。

4.3 俯视摄像头标定

[6]对于上面的情况摄像头需要保持一个固定高度,在一些情况下不是非常方便,还有一种其实类似的情况就是当摄像头只是保持一个角度俯视,而不是固定平行向下,这个时候的标定就涉及到根据现实情况计算Z,这就涉及到了一个单目摄像头测距的问题。

在这里插入图片描述

目标:输入一个像素坐标点,然后计算出该像素点实际位置距离摄像头水平距离和垂直距离,即实现了单目摄像头测距
摄像头固定角度,倾斜照射到地面,先不考虑镜头畸变

在这里插入图片描述

需要提前测得的量:
H:摄像头高度
Dmin:图像底边距摄像头实际距离
Dmax:图像顶边距摄像头实际距离
图像像素高度:480
图像像素宽度:600
输入:
X0:要测量的点的像素X坐标
Y0:要测量的点的像素Y坐标
输出:
X1:该点距摄像头的水平距离
Y1:该点距摄像头的垂直距离

在这里插入图片描述
H^2+〖Y1〗^2=Z^2

算出Z以后使用标定好的矩阵直接计算也可以通过像素坐标计算实际坐标。

4.4 本章小结

本章分析了摄像头的成像原理,成像原理与小孔成像的原理是一样的。分析出像素坐标,摄像头坐标,世界坐标的转换关系。基于单目摄像头不能获取深度信息,我们通过两种模式:分别是固定向下和俯视向下的现实模型先计算出深度信息,然后通过标定矩阵,构建像素坐标与世界坐标之间的关系。

【机器人原理与实践(一)】ROS学习笔记

@[toc]

常用命令

启动ROS: roscore

获取节点信息:
rosnode list 列出所有正在运行的节点
rosnode info xxx 列出节点信息
rosnode ping
rosnode machine
rosnode kill

运行节点
rosrun XXX

创建ROS功能包:roscreate-pkg XXX std_msgs rospy roscpp

编译功能包 :rosmake xxx

查找路径:rospack find xxx

查看依赖关系 :rospack depends XXX

查看功能包内容:rosls

更改包路径 :roscd

显示观察话题:rostopic list
rostopic echo xxx -n x

放码过来

Python 编程

#!/usr/bin/env python
import rospy
from std_msgs.msg import Int32

rospy.init_node('topic_publisher')
pub=rospy.Publisher('PS2',Int32)
rate=rospy.Rate(2)
count=0
while not rospy.is_shutdown():
    pub.publish(count)
    count+=1
    rate.sleep()
#!/usr/bin/env python
import rospy
from std_msgs.msg import Int32

def callback(msg):
    print(msg.data)

rospy.init_node('subsribe')
sub=rospy.Subscriber('PS2',Int32,callback)
rospy.spin()

C++编程

ROS的Hello World

消息发布者

#include <ros/ros.h>
#include <std_msgs/String.h>
#include <sstream>
int main(int argc,char ** argv)
{
    //启动节点并设置名称
    ros::init(argc,argv,"ps2");
    //节点句柄
    ros::NodeHandle n;
    ROS_INFO_STREAM("PS2 NODE INIT");
    //设置节点为发布者,并将发布的主题类型告知节点管理器
    //参数1:消息名称
    //参数2:缓冲区大小
    ros::Publisher pub=n.advertise<std_msgs::String>("PS2",1000);
    //一秒发送10次
    ros::Rate loop_rate(10);
    while(ros::ok()){
        std_msgs::String msg;
        std::stringstream ss;
        ss<<"I am PS node";
        msg.data=ss.str();
        //发布消息
        pub.publish(msg);
        ros::spinOnce();
        loop_rate.sleep();
    }
    return 0;
}

消息订阅者

#include <ros/ros.h>
#include <std_msgs/String.h>

void callBack(const std_msgs::String::ConstPtr& msg)
{
    ROS_INFO("GetPS2 I head that: [%s]",msg->data.c_str());
}

int main(int argc ,char** argv)
{
    ros::init(argc,argv,"GetPS2");
    ros::NodeHandle n;
    ros::Subscriber sub=n.subscribe("PS2",1000,callBack);
    ros::spin();
    return 0;
}

就像上面说的,ros::spin() 在调用后不会再返回,也就是你的主程序到这儿就不往下执行了,而 ros::spinOnce() 后者在调用后还可以继续执行之后的程序。
其实看函数名也能理解个差不多,一个是一直调用;另一个是只调用一次,如果还想再调用,就需要加上循环了。
这里一定要记住,ros::spin()函数一般不会出现在循环中,因为程序执行到spin()后就不调用其他语句了,也就是说该循环没有任何意义,还有就是spin()函数后面一定不能有其他语句(return 0 除外),有也是白搭,不会执行的。ros::spinOnce()的用法相对来说很灵活,但往往需要考虑调用消息的时机,调用频率,以及消息池的大小,这些都要根据现实情况协调好,不然会造成数据丢包或者延迟的错误。

这里需要特别强调一下,如果大兄弟你的程序写了相关的消息订阅函数,那千万千万千万不要忘了在相应位置加上ros::spin()或者ros::spinOnce()函数,不然你是永远都得不到另一边发出的数据或消息的,博主血的教训,万望紧记。。。

创建msg与srv文件

msg与src勇于定义传输数据的类型与数据值的文件。ROS会根据这些文件内容自动的为我们创建所需的代码。
使用ROS创建自定义消息

在功能包文件夹下创建msg文件夹,然后在msg文件夹中创建.msg文件 输入
int32 A
int32 B
int32 C

编辑CMakeList.txt

显示消息情况
rosmsg show arm/PS2

显示服务情况
rossrv show arm/PS_2

使用srv与msg

#include <ros/ros.h>
#include "arm/PS2.h"
#include <sstream>
int main(int argc,char** argv)
{
    ros::init(argc,argv,"sendMsg");
    ros::NodeHandle n;
    ros::Publisher pub=n.advertise<arm::PS2>("PS2",1000);
    ros::Rate loop_rate(10);
    while(ros::ok())
    {
        arm::PS2 msg;
        msg.A=1;
        msg.B=1;
        msg.C=1;
        pub.publish(msg);
        ros::spinOnce();
        loop_rate.sleep();
    }
    return 0;
}
#include <ros/ros.h>
#include "arm/PS2.h"

void callBack(const arm::PS2::ConstPtr& msg )
{
    ROS_INFO("I Heard :[%d][%d][%d]",msg->A,msg->B,msg->C);
}

int main(int argc ,char** argv)
{
    ros::init(argc,argv,"getMsg");
    ros::NodeHandle n;
    ros::Subscriber sub =n.subscribe("PS2",1,callBack);
    ros::spin();
    return 0;
}

【Jetson nano开发笔记】jetson nano 环境搭建与常见软件安装

@[toc]

开发板

看看他香不香

Jetson Nano 2GB是一款单板计算机,具有四核1.4GHz ARM CPU和内置的Nvidia Maxwell GPU。它是最便宜的Nvidia Jetson机型,针对的是购买树莓派的业余爱好者。

那里有很多AI开发板和加速器模块,但Nvidia拥有一大优势——它与桌面AI库直接兼容,不需要你将深度学习模型转换为任何特殊格式即可运行他们。

它使用几乎所有每个基于Python的深度学习框架都已使用的相同的CUDA库进行GPU加速。这意味着你可以采用现有的基于Python的深度学习程序,几乎无需修改就可以在Jetson Nano 2GB上运行它,并且仍然可以获得良好的性能(只要你的应用程序可以在2GB的RAM上运行)。

它将为强大的服务器编写的Python代码部署在价格为59美元的独立设备上的能力非常出色。

这款新的Jetson Nano 2GB主板也比Nvidia以前的硬件版本更加光鲜亮丽。

第一个Jetson Nano机型莫名其妙地缺少WiFi,但该机型随附一个可插入的WiFi模块,因此你不必再加上杂乱的以太网电缆了。他们还将电源输入升级到了更现代的USB-C端口,并且在软件方面,一些粗糙的边缘已被磨掉。例如,你无需执行诸如启用交换文件之类的基本操作。

Nvidia积极地推出了一款价格低于60美元的带有真实GPU的简单易用的硬件设备。似乎他们正以此为目标瞄准树莓派,并试图占领教育/爱好者市场。看看市场如何反应将是很有趣的。

根据NVIDIA官方提供的性能对照表,2GB版在不同AI软件的性能表现能达到Raspberry Pi 4的数十倍之谱。(图中将Raspberry Pi 4的性能表现标准化为1)

在操作系统部分,笔者使用NVIDIA提供的最新版本Jetson Nano镜像文件,安装完成后会发现它的桌面环境改为LXDE18.04,与先前的Ubuntu接口有些许不同,但是整体操作仍相当接近。

值得注意的是,在第一次开机的初始设置过程中,系统会建议用户激活Swap功能,若选择默认选项,系统则会自动创建容量约为4.95GB的换置空间,以在主内存容量不足的时候存储资料,发挥虚拟内存的功效。不过由于Swap的实际路径为microSD内存卡,所以带宽与访问性能将受到很大的限制,而且反复访问也可能会加速耗损内存卡寿命。

性能表:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YPSNVHXF-1621171965108)(https://pics5.baidu.com/feed/f636afc379310a555619aba4fab2cbae8326109b.jpeg?token=f1b5a967f7a80b08688e33bb7c8062d1)]
在这里插入图片描述
在这里插入图片描述

最新版操作系统将桌面环境改为LXDE 18.04,与先前的Ubuntu接口有些不同。
我感觉用这个桌面系统主要原因就是在不用到桌面很多功能的时候可以减少系统开销

55美元在某宝上大概420元左右就可以搞一手,和最新的树莓派4价格差不多。可以接触一些树莓派根本学不到的东西比如 cuda ,和一堆需要用到GPU的功能,你说香不香呢。

但是也有缺点:

  1. 资料很少,仅有非常少的开发文档,而官方给的一些东西都是英文,我这个英语菜鸡表示有点难受。
  2. 支持的系统有限,只有ubuntu
  3. 没有自带的WIFI和蓝牙如果不想拉网线的话只能多买一个适配器了。

IO定义

设置一下root密码吧

登录以后只是一般用户一些东西并不是非常方便。

sudo passwd root

更换源

deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ bionic main multiverse restricted universe
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ bionic-security main multiverse restricted universe
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ bionic-updates main multiverse restricted universe
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ bionic-backports main multiverse restricted universe
deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ bionic main multiverse restricted universe
deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ bionic-security main multiverse restricted universe
deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ bionic-updates main multiverse restricted universe
deb-src http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ bionic-backports main multiverse restricted universe

apt-get update

赶紧配置VNC

在开发之前首先需要配置一下VNC,不然你会在电脑键盘和jetson 的键盘之间切来切去非常麻烦。

如何安装

jetson nano官方镜像中已安装有vnc软件vino-server,只需进行一些配置即可使用。

1、确认系统已经安装好后打开系统设置,找到桌面共享,你会发现打不开,据说这是个bug。

解决方法:
第一步:在终端输入这句指令编辑此文件

sudo gedit /usr/share/glib-2.0/schemas/org.gnome.Vino.gschema.xml

第二步:滑到最后面文末的位置,将下面这段内容全部粘贴进去

<key name='enabled' type='b'>
      <summary>Enable remote access to the desktop</summary>
      <description>
        If true, allows remote access to the desktop via the RFB
        protocol. Users on remote machines may then connect to the
        desktop using a VNC viewer.
      </description>
      <default>false</default>
    </key>

第三步:输入以下指令编译一下刚才编辑过的文件

sudo glib-compile-schemas /usr/share/glib-2.0/schemas

2、打开桌面共享设置后按照如下配置即可,密码不用太复杂,好像不设置也可以。

3、 开机设置

4、 配置VNC设置

gsettings set org.gnome.Vino prompt-enabled false
gsettings set org.gnome.Vino require-encryption false

已安装组件的使用

Jetson-nano的OS镜像已经自带了JetPack,cuda,cudnn,opencv等都已经安装好,并有例子,这些例子安装路径如下所示

TensorRT/usr/src/tensorrt/samples/
CUDA/usr/local/cuda-/samples/
cuDNN/usr/src/cudnn_samples_v7/
Multimedia API/usr/src/tegra_multimedia_api/
VisionWorks/usr/share/visionworks/sources/samples/ /usr/share/visionworks-tracking/sources/samples/ /usr/share/visionworks-sfm
OpenCV/usr/share/OpenCV/samples/

cuda

Jetson-nano中已经安装了CUDA10.2版本,但是此时你如果运行 nvcc -V是不会成功的,需要你把CUDA的路径写入环境变量中。OS中自带Vim工具 ,所以运行下面的命令编辑环境变量

sudo vim ~/.bashrc

在最后添加

export CUDA_HOME=/usr/local/cuda-10.2
export LD_LIBRARY_PATH=/usr/local/cuda-10.2/lib64:$LD_LIBRARY_PATH
export PATH=/usr/local/cuda-10.2/bin:$PATH

然后保存退出

对了最后别忘了source一下这个文件。

source ~/.bashrc

source后,此时再执行nvcc -V执行结果如下
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2018 NVIDIA Corporation
Built on Sun_Sep_30_21:09:22_CDT_2018
Cuda compilation tools, release 10.0, V10.0.166
beckhans@Jetson:~$

OpenCV

Jetson-nano中已经安装了OpenCV4.1版本,可以使用命令检查OpenCV是否安装就绪

但是比较可惜的是这些库没有CUDA以及扩展模块如果需要的话需要自己编译

pkg-config opencv4 –modversion

编译
g++ xxx.cpp -o xxx -L /usr/lib/aarch64-linux-gnu/libopencv* -I /usr/include/opencv4/

cuDNN

cd /usr/src/cudnn_samples_v7/mnistCUDNN #进入例子目录
sudo make #编译一下例子
sudo chmod a+x mnistCUDNN # 为可执行文件添加执行权限
./mnistCUDNN # 执行

cudnnGetVersion() : 7301 , CUDNN_VERSION from cudnn.h : 7301 (7.3.1)
Host compiler version : GCC 7.3.0
There are 1 CUDA capable devices on your machine :
device 0 : sms  1  Capabilities 5.3, SmClock 921.6 Mhz, MemSize (Mb) 3964, MemClock 12.8 Mhz, Ecc=0, boardGroupID=0
Using device 0

Testing single precision
Loading image data/one_28x28.pgm
Performing forward propagation ...
Testing cudnnGetConvolutionForwardAlgorithm ...
Fastest algorithm is Algo 1
Testing cudnnFindConvolutionForwardAlgorithm ...
^^^^ CUDNN_STATUS_SUCCESS for Algo 1: 0.325104 time requiring 3464 memory
^^^^ CUDNN_STATUS_SUCCESS for Algo 0: 0.387500 time requiring 0 memory
^^^^ CUDNN_STATUS_SUCCESS for Algo 2: 0.540729 time requiring 57600 memory
^^^^ CUDNN_STATUS_SUCCESS for Algo 4: 4.965156 time requiring 207360 memory
^^^^ CUDNN_STATUS_SUCCESS for Algo 7: 5.201146 time requiring 2057744 memory
Resulting weights from Softmax:
0.0000000 0.9999399 0.0000000 0.0000000 0.0000561 0.0000000 0.0000012 0.0000017 0.0000010 0.0000000 
Loading image data/three_28x28.pgm
Performing forward propagation ...
Resulting weights from Softmax:
0.0000000 0.0000000 0.0000000 0.9999288 0.0000000 0.0000711 0.0000000 0.0000000 0.0000000 0.0000000 
Loading image data/five_28x28.pgm
Performing forward propagation ...
Resulting weights from Softmax:
0.0000000 0.0000008 0.0000000 0.0000002 0.0000000 0.9999820 0.0000154 0.0000000 0.0000012 0.0000006 

Result of classification: 1 3 5

Test passed!

Testing half precision (math in single precision)
Loading image data/one_28x28.pgm
Performing forward propagation ...
Testing cudnnGetConvolutionForwardAlgorithm ...
Fastest algorithm is Algo 1
Testing cudnnFindConvolutionForwardAlgorithm ...
^^^^ CUDNN_STATUS_SUCCESS for Algo 0: 0.113750 time requiring 0 memory
^^^^ CUDNN_STATUS_SUCCESS for Algo 1: 0.119792 time requiring 3464 memory
^^^^ CUDNN_STATUS_SUCCESS for Algo 2: 0.236198 time requiring 28800 memory
^^^^ CUDNN_STATUS_SUCCESS for Algo 4: 1.031719 time requiring 207360 memory
^^^^ CUDNN_STATUS_SUCCESS for Algo 5: 5.049948 time requiring 203008 memory
Resulting weights from Softmax:
0.0000001 1.0000000 0.0000001 0.0000000 0.0000563 0.0000001 0.0000012 0.0000017 0.0000010 0.0000001 
Loading image data/three_28x28.pgm
Performing forward propagation ...
Resulting weights from Softmax:
0.0000000 0.0000000 0.0000000 1.0000000 0.0000000 0.0000714 0.0000000 0.0000000 0.0000000 0.0000000 
Loading image data/five_28x28.pgm
Performing forward propagation ...
Resulting weights from Softmax:
0.0000000 0.0000008 0.0000000 0.0000002 0.0000000 1.0000000 0.0000154 0.0000000 0.0000012 0.0000006 

Result of classification: 1 3 5

Test passed!

jetson nano 安装 TensorFlow GPU

安装pip

因为Jetson Nano中已经安装了Python3.6版本,所以安装pip还是比较简单的

sudo apt-get install python3-pip python3-dev

安装后pip是9.01版本,需要把它升级到最新版,升级后pip版本为19.0.3。这里面升级后会有一个小Bug,需要手动改一下

python3 -m pip install –upgrade pip #升级pip

sudo vim /usr/bin/pip3 #打开pip3文件

将原来的

from pip import main
if __name__ == '__main__':
    sys.exit(main())

改成

from pip import __main__
if __name__ == '__main__':
    sys.exit(__main__._main())

修改结束后保存。运行pip3 -V成功后显示

pip 21.1.1 from /usr/local/lib/python3.6/dist-packages/pip (python 3.6)

安装那些机器学习领域的包

sudo apt-get install python3-scipy
sudo apt-get install python3-pandas
sudo apt-get install python3-sklearn

pip3 install CPython #必须有这一步,否则h5py编译会出错

安装TensorFlow GPU版

(1)确认CUDA已经被正常安装

nvcc -V

如果能看到CUDA版本号,即为正确安装

(2)安装所需要的包

sudo apt-get install python3-pip libhdf5-serial-dev hdf5-tools

sudo apt-get install libhdf5-serial-dev hdf5-tools libhdf5-dev zlib1g-dev zip libjpeg8-dev liblapack-dev libblas-dev gfortran
pip3 install -U pip
pip3 install -U CPython testresources setuptools

pip3 install -U numpy==1.16.1 future==0.17.1 mock==3.0.5 h5py==2.9.0 keras_preprocessing==1.0.5 keras_applications==1.0.8 gast==0.2.2 futures protobuf pybind11

(3)安装TensorFlow GPU版本

pip3 install /home/yuri/WorkSpace/TensorFlow/tensorflow-2.2.0+nv20.6-cp36-cp36m-linux_aarch64.whl

至于这个轮文件可以到英伟达官方下载链接下载

wget https://developer.download.nvidia.com/compute/redist/jp/v44/tensorflow/tensorflow-2.2.0+nv20.6-cp36-cp36m-linux_aarch64.whl

ROS安装

什么是ROS

ROS是一个机器人操作系统,大家可以想一想Windows操作系统,Windows系统可以打开文件夹,进行文件管理,播放多媒体,用户按照Windows系统开放的接口,可以编写Windows应用程序。ROS系统也是一样,它封装了一些对机器人控制的基本接口,只要我们遵循这些接口制定的规则,我们也可以开发出机器人控制应用。

选用ROS系统

主流的ROS有Melodic Morenia,ROS Kinetic Kame等等,大家可以理解为Liunx系统平台下也分为Ubuntu,CentOS等等。由于主流系统中目前只有Melodic Morenia支持Ubuntu18.04,所以也没得选,就Melodic Morenia了。

准备工作

其实也没啥准备的,这里有个小建议,就是准备一张新的SD卡安装ROS,一是因为ROS挺大,二是如果装了ROS,又拿同一套系统建立模型,会影响速度。SD卡建议32G以上。

开始安装

将安装源添加到source.list

sudo sh -c ‘echo “deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main” > /etc/apt/sources.list.d/ros-latest.list’

添加公钥和更新系统

sudo apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654

sudo apt update

安装ROS

sudo apt install ros-melodic-desktop-full

上面的命令是把ROS, rqt, rviz, robot-generic libraries, 2D/3D simulators一起安装了,你也可以单独安装,我就懒得写了,自己查官网去吧。

初始化ROS

sudo rosdep init
rosdep update

将ROS配置信息写入环境变量,保证每一个终端启动时,都会自动执行setup.bash

echo "source /opt/ros/melodic/setup.bash" >> ~/.bashrc
source ~/.bashrc

安装一些依赖包

sudo apt install python-rosinstall python-rosinstall-generator python-wstool build-essential

测试一下ROS是否正确安装,输入下面的命令

roscore

如果没有访问外服的能力一定会遇到的问题

说白了就是在DNS解析的时候,直接访问127.0.0.1
可以自己ping 一下试试

解决方案0x01

如果你正遇到这个问题,那么第一个解决方案就是,更换网络,你可以尝试使用你的手机热点,然后继续尝试以下指令。

sudo rosdep init
rosdep update

解决方案0x02

该解决方案是针对由于以下两个无法正常访问,但可以ping通,于是修改hosts文件,加入以下两个网址的IP地址实现访问。

sudo gedit /etc/hosts

199.232.28.133 raw.githubusercontent.com
151.101.228.133 raw.github.com

修改完成后,在终端执行

sudo rosdep init
rosdep update

掉坑经验

经过测试用校园网访问不超过,移动手机热点访问不成功,电信手机热点访问成功

安装pygame

$ sudo apt-get install libsdl-image1.2-dev ffmpeg libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsmpeg-dev libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev

$ sudo apt-get -y install libfreetype6-dev

$ pip3 install pygame

增加SWAP

Nano入门教程基础篇-增加swap空间

说明:

介绍如何增加swap空间

步骤:

生成swapfile文件

sudo fallocate -l 4G /var/swapfile
sudo chmod 600 /var/swapfile
sudo mkswap /var/swapfile
sudo swapon /var/swapfile

设置为自动启用swapfile

sudo bash -c ‘echo “/var/swapfile swap swap defaults 0 0” >> /etc/fstab’

【数字图像处理】OpenCV3 学习笔记

1. 加载保存显示图像

Mat imread( const String& filename, int flags = IMREAD_COLOR );

bool imwrite( const String& filename, InputArray img,
              const std::vector<int>& params = std::vector<int>());

void cvtColor( InputArray src, OutputArray dst, int code, int dstCn = 0 );

2. 操作图像像素

2.1. 方法1:使用 Mat 中对矩阵元素的地址定位的知识

step[0]:一行字节数
step[1]:一个像素字节数

    int main()
    {
        //新建一个uchar类型的单通道矩阵(grayscale image 灰度图)
        Mat m(400, 400, CV_8U, Scalar(0));
        for (int col = 0; col < 400; col++)
        {
            for (int row = 195; row < 205; row++)
            {
                cout << (int)(*(m.data + m.step[0] * row + m.step[1] * col)) << "  ==> ";
                //获取第[row,col]个像素点的地址并用 * 符号解析
                *(m.data + m.step[0] * row + m.step[1] * col) = 255;
                cout << (int)(*(m.data + m.step[0] * row + m.step[1] * col)) << endl;
            }
        }
        imshow("canvas", m);
        cvWaitKey();
        return 0;
    }

2.2. 方法二:使用 Mat::at 函数

    int main()
    {    
        Mat img = imread("lena.jpg");
        imshow("Lena Original", img);
        for (int row = 0; row < img.rows; row++)
        {
            for (int col = 0; col < img.cols; col++)
            {    
                /* 注意 Mat::at 函数是个模板函数, 需要指明参数类型, 因为这张图是具有红蓝绿三通道的图,
                   所以它的参数类型可以传递一个 Vec3b, 这是一个存放 3 个 uchar 数据的 Vec(向量). 这里
                   提供了索引重载, [2]表示的是返回第三个通道, 在这里是 Red 通道, 第一个通道(Blue)用[0]返回 */
                if(img.at<Vec3b>(row, col)[2] > 128)
                    img.at<Vec3b>(row, col) = Vec3b(255, 255, 255);
            }
        }
        imshow("Lena Modified", img);
        cvWaitKey();
        return 0;
    }

2.3. 方法三:使用 Mat 的一个模板子类 Mat_

    int main()
    {    
        Mat m(400, 400, CV_8UC3, Scalar(255, 255, 255));
        // m2 是 Mat_<Vec3b> 类型的, 因为 m 中元素的类型是 CV_8UC3, 可以用 Vec3b 存储 3 个通道的值
        // 注意 Mat_<CV_8UC3> 这种写法是错误的, 因为 CV_8UC3 只是一个宏定义
        // #define CV_8UC3 CV_MAKETYPE(CV_8U, 3)
        Mat_<Vec3b> m2 = m;
        // for 循环画一个红色的实心圆
        for (int y = 0; y < m.rows; y++)
        {
            for (int x = 0; x < m.rows; x++)
            {
                if (pow(double(x-200), 2) + pow(double(y-200), 2) - 10000.0 < 0.00000000001)
                {
                    // Mat_ 模板类实现了对()的重载, 可以定位到一个像素
                    m2(x, y) = Vec3b(0, 0, 255);
                }
            }
        }
        imshow("Image", m);
        cvWaitKey();
        return 0;
    }

2.4. 方法四:使用 Mat::ptr 模板函数

    int main()
    {    
        Mat m(400, 400, CV_8UC3, Scalar(226, 46, 166));
        imshow("Before", m);
        for (int row = 0; row < m.rows; row++)
        {
            if (row % 5 == 0)
            {
                // data 是 uchar* 类型的, m.ptr<uchar>(row) 返回第 row 行数据的首地址
                // 需要注意的是该行数据是按顺序存放的,也就是对于一个 3 通道的 Mat, 一个像素有
                // 有 3 个通道值, [B,G,R][B,G,R][B,G,R]... 所以一行长度为:
                // sizeof(uchar) * m.cols * m.channels() 个字节
                uchar* data = m.ptr<uchar>(row);
                for (int col = 0; col < m.cols; col++)
                {
                    data[col * 3] = 102; //第row行的第col个像素点的第一个通道值 Blue
                    data[col * 3 + 1] = 217; // Green
                    data[col * 3 + 2] = 239; // Red
                }
            }
        }
        imshow("After", m);
        cout << (int)m.at<Vec3b>(0, 0)[0] << ','; //利用 Fn 1 介绍的方法输出一下像素值到控制台
        cout << (int)m.at<Vec3b>(0, 0)[1] << ',';
        cout << (int)m.at<Vec3b>(0, 0)[2] << endl;
        cvWaitKey();
        return 0;

    }

3. 图像掩模

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-02 23:45:24
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-03 00:50:40
 */
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
using namespace std;
int main()
{
    Mat srcImage,dstImage;
    srcImage=imread("F:/SDK/OpenCV/HiKivision/picture/lena.jpg");
    //if(!srcImage.data){cout<<"load error"<<endl;}
    // int cols=(srcImage.cols-1)*srcImage.channels();
    // int rows=srcImage.rows;
    // int offset=srcImage.channels();
    // dstImage=Mat::zeros(srcImage.size(),srcImage.type());
    // for(int row=1;row<rows-1;row++)
    // {
    //     const uchar* priviousptr=srcImage.ptr<uchar>(row-1);
    //     const uchar* currentptr =srcImage.ptr<uchar>(row);
    //     const uchar* nextptr    =srcImage.ptr<uchar>(row+1);
    //     uchar* outptr     =dstImage.ptr<uchar>(row);
    //     /**
    //      * 相当于卷积
    //      *    -1
    //      * -1  5  -1
    //      *    -1
    //     */
    //     for(int col=offset;col<cols;col++)
    //     {
    //         outptr[col]=saturate_cast<uchar>( 5*currentptr[col]-currentptr[col-offset]-currentptr[col+offset]-priviousptr[col]-nextptr[col]);
    //     }
    // }

    Mat kernel =(Mat_<char>(3,3)<<0,-1,0,-1,5,-1,0,-1,0);
    filter2D(srcImage,dstImage,srcImage.depth(),kernel);

    imwrite("F:/SDK/OpenCV/HiKivision/picture/mask.jpg",dstImage);
    return 0;
}

4. 图像线性变换

4.1. 图像线性混合

g(x,y)=af1(x,y)+bf2(x,y)+gamma

void addWeighted(InputArray src1, double alpha, InputArray src2,
                              double beta, double gamma, OutputArray dst, int dtype = -1);
/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-03 01:24:41
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-03 01:35:19
 */
#include <opencv2/opencv.hpp>
using namespace cv;
int main()
{
    Mat srcImage1,srcImage2,dstImage;
    srcImage1=imread("F:/SDK/OpenCV/HiKivision/picture/lena.jpg");
    srcImage2=imread("F:/SDK/OpenCV/HiKivision/picture/apple.jpg");

    addWeighted(srcImage1,0.5,srcImage2,0.5,0,dstImage);
    imwrite("F:/SDK/OpenCV/HiKivision/picture/aadd.jpg",dstImage);
    multiply(srcImage1,srcImage1,dstImage,0.5);
    imwrite("F:/SDK/OpenCV/HiKivision/picture/muliy.jpg",dstImage);

    return 0;
}

4.2. 图像亮度与对比度

g(x,y)=a*f(x,y)+b
上面这个公式可以很好的解释对图像的亮度和对比度操作的原理,第一个参数α必须是大于零,不然则基本上没有意义了。
α能代表什么呢?α能使图像像素成倍数的增长或降低(α<1),改变了是图像的对比度,因为使图像的差值变化了。
那么β作何解释呢?β可为负,也可为正,那么任何一个像素都在(0, 255)之间,加上一个值或减去一个值则会使这个
像素点变大或变小,其实就是向白色或向黑色靠近(0为黑,255为白),所以改变的是图像的亮度。

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-03 01:42:42
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-03 08:54:29
 */
#include <opencv2/opencv.hpp>
using namespace cv;
int main()
{
    Mat srcImage,dstImage;
    srcImage=imread("F:/SDK/OpenCV/HiKivision/picture/lena.jpg");
    dstImage=Mat::zeros(srcImage.size(),srcImage.type());
    int height =srcImage.rows;
    int width=srcImage.cols;
    //1.2*pix+30
    float alpha=1.2;
    float beta=50;

    for(int row=0;row<width;row++)
    {
        for(int col=0;col<width;col++)
        {
            if(srcImage.channels()==3){
                float b=srcImage.at<Vec3b>(row,col)[0];
                float g=srcImage.at<Vec3b>(row,col)[1];
                float r=srcImage.at<Vec3b>(row,col)[2];
                dstImage.at<Vec3b>(row,col)[0]=saturate_cast<uchar> (b*alpha+beta);
                dstImage.at<Vec3b>(row,col)[1]=saturate_cast<uchar> (g*alpha+beta);
                dstImage.at<Vec3b>(row,col)[2]=saturate_cast<uchar> (r*alpha+beta);
            }else if(srcImage.channels()==4){
                float b=srcImage.at<Vec4b>(row,col)[0];
                float g=srcImage.at<Vec4b>(row,col)[1];
                float r=srcImage.at<Vec4b>(row,col)[2];
                dstImage.at<Vec4b>(row,col)[0]=saturate_cast<uchar> (b*alpha+beta);
                dstImage.at<Vec4b>(row,col)[1]=saturate_cast<uchar> (g*alpha+beta);
                dstImage.at<Vec4b>(row,col)[2]=saturate_cast<uchar> (r*alpha+beta);
                dstImage.at<Vec4b>(row,col)[3]=srcImage.at<Vec4b>(row,col)[3];
            }else{
                float b=(float)srcImage.at<uchar>(row,col);
                dstImage.at<Vec3b>(row,col)=b*alpha+beta;        
            }
        }
    }
    imwrite("F:/SDK/OpenCV/HiKivision/picture/linghtchange.jpg",dstImage);
    return 0;
}

4.3. 线性模糊

4.3.1. 均值模糊

void blur( InputArray src, OutputArray dst,
Size ksize, Point anchor = Point(-1,-1),
int borderType = BORDER_DEFAULT );

4.3.2. 高斯模糊

void GaussianBlur(InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY=0, int borderType=BORDER_DEFAULT )

InputArray src:输入的图像
OutputArray dst:输出图像
Size ksize:高斯卷积核的大小,是奇数
double sigmaX, double sigmaY=0, :表示x和y方向的方 差,如果y=0则y方向的方差与x相等
int borderType=BORDER_DEFAULT :边界的处理方式,一般默认

高斯模糊:越靠近卷积核的领域权重越大。
均值模糊:领域权重都为1。
而无论是高斯模糊或者是均值模糊,有一个缺点是他们在模糊的时候并不能很好的保留
边缘信息。因此双边滤波便很好客服了这一缺陷。原理如图:

void bilateralFilter(InputArray src, OutputArray dst, int d, double sigmaColor, double sigmaSpace, int borderType=BORDER_DEFAULT )
InputArray src: 输入图像,可以是Mat类型,图像必须是8位或浮点型单通道、三通道的图像。
. OutputArray dst: 输出图像,和原图像有相同的尺寸和类型。
. int d: 表示在过滤过程中每个像素邻域的直径范围。如果这个值是非正数,则函数会从第五个参数sigmaSpace计算该值。
. double sigmaColor: 颜色空间过滤器的sigma值,这个参数的值月大,表明该像素邻域内有月宽广的颜色会被混合到一起,产生较大的半相等颜色区域。
. double sigmaSpace: 坐标空间中滤波器的sigma值,如果该值较大,则意味着颜色相近的较远的像素将相互影响,从而使更大的区域中足够相似的颜色获取相同的颜色。当d>0时,d指定了邻域大小且与sigmaSpace五官,否则d正比于sigmaSpace.
. int borderType=BORDER_DEFAULT: 用于推断图像外部像素的某种边界模式,有默认值BORDER_DEFAULT.

5. 形态学操作

形态学元素
Mat getStructuringElement(int shape, Size ksize, Point anchor = Point(-1,-1));

5.1. 腐蚀膨胀

膨胀
void dilate( InputArray src, OutputArray dst, InputArray kernel,
Point anchor = Point(-1,-1), int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() );
腐蚀
void erode( InputArray src, OutputArray dst, InputArray kernel,
Point anchor = Point(-1,-1), int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() );

跟卷积操作类似,假设有图像A和结构元素B,结构元素B在A上面移动,其中B定义其中心为锚点,计算B覆盖下A的最大像素值用来替换锚点的像素,其中B作为结构体可以是任意形状。我们回忆一下中值平滑操作——取每一个位置的矩形领域内值的中值作为该位置的输出灰度值,图像的膨胀操作与中值平滑操作类似,它是取每一个位置的矩形领域内值的最大值作为该位置的输出灰度值。不同的是,这里的领域不再单纯是矩形结构的,也可以是椭圆形结构的、十字交叉形结构的等,其中红色是参考点,也称为锚点(anchor point),如下所示:

因此取每个位置领域内最大值,所以膨胀后输出图像的总体亮度的平均值比起原图会有所升高,图像中比较亮的区域的面积会变大,而较暗物体的尺寸会减小甚至消失

腐蚀操作与膨胀操作类似,只是它取结构元所指定的领域内值的最小值作为该位置的输出灰度值。因为取每个位置领域内最小值,所以腐蚀后输出图像的总体亮度的平均值比起原图会有所降低,图像中比较亮的区域的面积会变小甚至消失,而较暗物体的尺寸会扩大

5.2. 开闭操作

cv2.morphologyEX(
img 输入图像
cv2.MORPH_CLOSE,cv2.MORPH_OPEN 形态学操作
kernel

用于对二值化后的图像进行处理,属于形态学操作(morphology)
开操作:消除白色的小点,去除小的干扰块
先对图像腐蚀后膨胀

闭操作:消除黑色的小块,填充闭合区域
先对图像膨胀后腐蚀

5.3. 顶帽黑帽

顶帽
就是开运算去掉的亮点
又称礼帽,是原始图像与进行开运算之后得到的图像的差。

因为开运算到来的结果是放大了裂痕或者局部低亮度的区域,因此,从原图中减去运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域,且这一操作和选择的核的大小相关。
顶帽运算往往用来分离比邻近点亮一些的斑块。当一幅图像具有大幅的背景的时候,而微小物品比较有规律的情况下,可以使用顶帽运算进行背景提取

黑帽
是进行闭运算以后得到的图像与原图像的差。
就是闭运算去掉的黑点
黑帽运算之后的效果图突出了与原图像轮廓周围的区域更暗的区域,且这一操作和选择的核大小相关。所以黑帽运算用来分离比邻近点暗一些的斑块。

5.4. 形态学梯度

形态学梯度是膨胀和腐蚀的差别,结果看上去就像前景物体的轮廓。计算的梯度常见的有三种:

基本梯度:
基本梯度是用膨胀后的图像减去腐蚀后的图像得到差值图像,称为梯度图像,也是OpenCV中支持的计算形态学梯度的方法,而此方法得到的梯度又被称为基本梯度。

内部梯度:
是用原图像减去腐蚀后的图像得到差值图像,称为图像的内部梯度

外部梯度:
图像膨胀后的图像减去原来的图像得到的差值图像,称为图像的外部梯度。

6. 图像金字塔

一个图像金字塔是一系列图像的集合 – 所有图像来源于同一张原始图像 – 通过梯次向下采样获得,直到达到某个终止条件才停止采样。
有两种类型的图像金字塔常常出现在文献和应用中:
高斯金字塔(Gaussian pyramid): 用来向下采样
拉普拉斯金字塔(Laplacian pyramid): 用来从金字塔低层图像重建上层未采样图像

6.1. 高斯金字塔

想想金字塔为一层一层的图像,层级越高,图像越小。

每一层都按从下到上的次序编号, 层级 (i+1) (表示为 G_{i+1} 尺寸小于层级 i (G_{i}))。

为了获取层级为 (i+1) 的金字塔图像,我们采用如下方法:

将 G_{i} 与高斯内核卷积:

将所有偶数行和列去除。

显而易见,结果图像只有原图的四分之一。通过对输入图像 G_{0} (原始图像) 不停迭代以上步骤就会得到整个金字塔。

以上过程描述了对图像的向下采样,如果将图像变大呢?:
首先,将图像在每个方向扩大为原来的两倍,新增的行和列以0填充(0)
使用先前同样的内核(乘以4)与放大后的图像卷积,获得 “新增像素” 的近似值。

这两个步骤(向下和向上采样) 分别通过OpenCV函数 pyrUp 和 pyrDown 实现, 我们将会在下面的示例中演示如何使用这两个函数。

#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <math.h>
#include <stdlib.h>
#include <stdio.h>

using namespace cv;

/// 全局变量
Mat src, dst, tmp;
char* window_name = "Pyramids Demo";


/**
 * @函数 main
 */
int main( int argc, char** argv )
{
  /// 指示说明
  printf( "\n Zoom In-Out demo  \n " );
  printf( "------------------ \n" );
  printf( " * [u] -> Zoom in  \n" );
  printf( " * [d] -> Zoom out \n" );
  printf( " * [ESC] -> Close program \n \n" );

  /// 测试图像 - 尺寸必须能被 2^{n} 整除
  src = imread( "../images/chicky_512.jpg" );
  if( !src.data )
    { printf(" No data! -- Exiting the program \n");
      return -1; }

  tmp = src;
  dst = tmp;

  /// 创建显示窗口
  namedWindow( window_name, CV_WINDOW_AUTOSIZE );
  imshow( window_name, dst );

  /// 循环
  while( true )
  {
    int c;
    c = waitKey(10);

    if( (char)c == 27 )
      { break; }
    if( (char)c == 'u' )
      { pyrUp( tmp, dst, Size( tmp.cols*2, tmp.rows*2 ) );
        printf( "** Zoom In: Image x 2 \n" );
      }
    else if( (char)c == 'd' )
     { pyrDown( tmp, dst, Size( tmp.cols/2, tmp.rows/2 ) );
       printf( "** Zoom Out: Image / 2 \n" );
     }

    imshow( window_name, dst );
    tmp = dst;
  }
  return 0;
}

7. 图像分割

7.1. 图像阈值化

double threshold( InputArray src, OutputArray dst,
double thresh, double maxval, int type );
参数说明
src:原始数组,可以是Mat类型。
dst:输出数组,必须与 src 的类型一致。
threshold:阈值
maxval:使用 CV_THRESH_BINARY 和 CV_THRESH_BINARY_INV 的最大值。
type:阈值类型

type=CV_THRESH_BINARY:如果 src(x,y)>threshold ,dst(x,y) = max_value; 否则,dst(x,y)=0;
type=CV_THRESH_BINARY_INV:如果 src(x,y)>threshold,dst(x,y) = 0; 否则,dst(x,y) = max_value.
type=CV_THRESH_TRUNC:如果 src(x,y)>threshold,dst(x,y) = max_value; 否则dst(x,y) = src(x,y).
type=CV_THRESH_TOZERO:如果src(x,y)>threshold,dst(x,y) = src(x,y) ; 否则 dst(x,y) = 0。
type=CV_THRESH_TOZERO_INV:如果 src(x,y)>threshold,dst(x,y) = 0 ; 否则dst(x,y) = src(x,y)

7.2. 图像锐化

三种锐化算子

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-02 23:45:24
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-03 11:39:23
 */
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
using namespace std;
int main()
{
    Mat srcImage,dstImage1,dstImage2,dstImage;
    Mat kernel;
    srcImage=imread("F:/SDK/OpenCV/HiKivision/picture/lena.jpg");
    cvtColor(srcImage,srcImage,CV_BGR2GRAY);
//ROBERT X
    kernel =(Mat_<int>(2,2)<<1,0,0,-1);
    filter2D(srcImage,dstImage1,srcImage.depth(),kernel);
    kernel =(Mat_<int>(2,2)<<0,1,-1,0);
    filter2D(srcImage,dstImage2,srcImage.depth(),kernel);
//ROBERT Y      
    imwrite("F:/SDK/OpenCV/HiKivision/picture/robertx.jpg",dstImage1);
    imwrite("F:/SDK/OpenCV/HiKivision/picture/roberty.jpg",dstImage2);
    dstImage= dstImage1+dstImage2;
    imwrite("F:/SDK/OpenCV/HiKivision/picture/robert.jpg",dstImage);

//SOBEL X
    kernel =(Mat_<int>(3,3)<<-1,0,1,-2,0,2,-1,0,1);
    filter2D(srcImage,dstImage1,srcImage.depth(),kernel);
    kernel =(Mat_<int>(3,3)<<-1,-2,-1,0,0,0,1,2,1);
    filter2D(srcImage,dstImage2,srcImage.depth(),kernel);
//SOBEL Y      
    imwrite("F:/SDK/OpenCV/HiKivision/picture/sobelx.jpg",dstImage1);
    imwrite("F:/SDK/OpenCV/HiKivision/picture/sobely.jpg",dstImage2);
    dstImage= dstImage1+dstImage2;
    imwrite("F:/SDK/OpenCV/HiKivision/picture/sobel.jpg",dstImage);

//LAPLACE
    kernel =(Mat_<int>(3,3)<<0,-1,0,-1,4,-1,0,-1,0);
    filter2D(srcImage,dstImage,srcImage.depth(),kernel);
    imwrite("F:/SDK/OpenCV/HiKivision/picture/laplace.jpg",dstImage);

    return 0;
}

7.3. Canny 边缘检测

void Canny( InputArray image, OutputArray edges,
double threshold1, double threshold2,
int apertureSize = 3, bool L2gradient = false );

详细原理见

Canny原理

7.4. 轮廓发现

void findContours( InputOutputArray image, OutputArrayOfArrays contours,
OutputArray hierarchy, int mode,
int method, Point offset = Point());

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-03 13:58:49
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-03 14:06:53
 */
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main() {
    Mat g_srcImage;
    Mat g_grayImage;
    Mat g_cannyMat_output;
    vector<vector<Point> > g_vContours;
    vector<Vec4i> g_vHierarchy;
    g_srcImage = imread("F:/SDK/OpenCV/HiKivision/picture/apple.jpg");
    if (!g_srcImage.data) {
        printf("图片读入错误!\n");
        return false;
    }
    cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
    blur(g_grayImage, g_grayImage, Size(3, 3));
    Canny(g_grayImage, g_cannyMat_output, 10, 120, 3);
    //寻找轮廓
    findContours(g_cannyMat_output, g_vContours, g_vHierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
    //绘出轮廓
    Mat drawing = Mat::zeros(g_cannyMat_output.size(), CV_8UC3);
    for (int i = 0; i < g_vContours.size(); i++) {
        drawContours(drawing, g_vContours, i, Scalar(0,0,255), 2, 8, g_vHierarchy, 0, Point());
    }
    imwrite("F:/SDK/OpenCV/HiKivision/picture/contour.jpg",drawing);
    return 0;
}

8. 霍夫变换

HOUGH变换原理

HoughLinesP原函数:

功能:将输入图像按照给出参数要求提取线段,放在lines中。

lines:是一个vector,Vec4i是一个包含4个int数据类型的结构体,[x1,y1,x2,y2],可以表示一个线段。
rho:就是一个半径的分辨率。
theta:角度分辨率。
threshold:判断直线点数的阈值。
minLineLength:线段长度阈值。
minLineGap:线段上最近两点之间的阈值。

//内部集成Canny
HoughCircles(re_noise,cv2.HOUGH_GRADIENT,1,20,param1==100,param2=30,minRadius=0,maxRadius=100)

参数说明:
InputArray: 输入图像,数据类型一般用Mat型即可,需要是8位单通道灰度图像

OutputArray:存储检测到的圆的输出矢量

method:使用的检测方法,目前opencv只有霍夫梯度法一种方法可用,该参数填HOUGH_GRADIENT即可(opencv 4.1.0下)

dp:double类型的dp,用来检测圆心的累加器图像的分辨率于输入图像之比的倒数,且此参数允许创建一个比输入图像分辨率低的累加器。上述文字不好理解的话,来看例子吧。例如,如果dp= 1时,累加器和输入图像具有相同的分辨率。如果dp=2,累加器便有输入图像一半那么大的宽度和高度。

minDist:为霍夫变换检测到的圆的圆心之间的最小距离

param1:它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示传递给canny边缘检测算子的高阈值,而低阈值为高阈值的一半。

param2:也是第三个参数method设置的检测方法的对应的参数,对当前唯一的方法霍夫梯度法HOUGH_GRADIENT,它表示在检测阶段圆心的累加器阈值。它越小的话,就可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就更加接近完美的圆形了。

minRadius:表示圆半径的最小值

maxRadius:表示圆半径的最大值

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-03 12:26:02
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-03 13:09:51
 */
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
#include <vector>
using namespace cv;
using namespace std;
int main()
{
    Mat srcImage,dstImage,gray;
    srcImage=imread("F:/SDK/OpenCV/HiKivision/picture/line.png");
    cvtColor(srcImage,srcImage,CV_BGR2GRAY);

    //边缘检测
    Canny(srcImage ,gray,100,130);
    imwrite("F:/SDK/OpenCV/HiKivision/picture/canny.jpg",dstImage);
    cvtColor(gray,dstImage,CV_GRAY2BGR);
    vector<Vec4f>lines;
    HoughLinesP(gray,lines,1,CV_PI/180,50);
    for(int i=0;i<lines.size();i++)
    {
        Vec4f hline=lines[i];
        line(dstImage,Point(hline[0],hline[1]),Point(hline[2],hline[3]),Scalar(0,0,255),3,LINE_AA);
    }
    imwrite("F:/SDK/OpenCV/HiKivision/picture/hough.jpg",dstImage);


    srcImage=imread("F:/SDK/OpenCV/HiKivision/picture/apple.jpg");
    cvtColor(srcImage,dstImage,CV_BGR2GRAY);
    vector<Vec3f>circles;
    HoughCircles(dstImage,circles,CV_HOUGH_GRADIENT,1,100,100,70,100,300);
    srcImage.copyTo(dstImage);
    for(int i=0;i<circles.size();i++)
    {
        Vec3f cc=circles[i];
        circle(dstImage,Point(cc[0],cc[1]),cc[2],Scalar(0,0,255),2,LINE_AA);
    }
    imwrite("F:/SDK/OpenCV/HiKivision/picture/houghc.jpg",dstImage);


    return 0;
}

9. 像素重映射

在opencv 中实现坐标变换的函数是remap,函数原型如下:

void cv::remap(InputArray src,
OutputArray dst,
InputArray map1,
InputArray map2,
int interpolation,
int borderMode = BORDER_CONSTANT,
const Scalar& borderValue = Scalar());

参数:

src : 原始图像;

dst : 目标图像,大小和map1的大小相同,数据类型和src的数据类型一样;

map1:表示(x,y)坐标点或者是x坐标,类型为CV_16SC2,CV_32FC1或者CV_32FC2;

map2: 表示y坐标,类型是CV_16UC1, CV_32FC1,当map1是(x,y)坐标时,map2可以为空;

Interpolation:表示插值算法,枚举类型主要。暂不支持INTER_AREA 插值算法。插值算法有一下几种:

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-03 13:15:17
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-03 13:17:09
 */
#include <iostream>
#include <opencv2/opencv.hpp>

int main(int argc,char* argv[])
{
    cv::Mat srcImage = cv::imread("F:/SDK/OpenCV/HiKivision/picture/lena.jpg");
    if (srcImage.empty())
    {
        std::cerr<< "fail to load image " <<std::endl;
        return -1;
    }
    cv::Mat xMapArray(srcImage.size(),CV_32FC1);
    cv::Mat yMapArray(srcImage.size(),CV_32FC1);
    cv::Mat dstImage(srcImage.size(),xMapArray.type());

    //设置变换x,y
    int rows = srcImage.rows;
    int cols = srcImage.cols;
    for(int i=0;i<rows;i++)
    {
        for(int j=0;j<cols;j++)
        {
            xMapArray.at<float>(i,j) = cols - j;
            yMapArray.at<float>(i,j) = rows - i;
        }
    }
    //进行变换
    cv::remap(srcImage,dstImage,xMapArray,yMapArray,cv::INTER_LINEAR,cv::BORDER_CONSTANT,cv::Scalar(0,0,0));
    cv::imwrite("F:/SDK/OpenCV/HiKivision/picture/remap.jpg",dstImage);
    return 0;
}

10. 模板匹配

模板匹配是在一幅图像中寻找一个特定目标的方法之一,这种方法的原理非常简单,遍历图像中的每一个可能的位置,比较各处与模板是否“相似”,当相似度足够高时,就认为找到了我们的目标。OpenCV提供了6种模板匹配算法:

平方差匹配法CV_TM_SQDIFF    
归一化平方差匹配法  CV_TM_SQDIFF_NORMED     
相关匹配法CV_TM_CCORR   
归一化相关匹配法CV_TM_CCORR_NORMED  
相关系数匹配法CV_TM_CCOEFF  
归一化相关系数匹配法    CV_TM_CCOEFF_NORMED 

用T表示模板图像,I表示待匹配图像,切模板图像的宽为w高为h,用R表示匹配结果,匹配过程如下图所示:

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-03 13:35:39
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-03 13:50:04
 */
/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-02 23:45:24
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-02 23:58:45
 */
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
    Mat srcImage,matchImage,result;
    srcImage=imread("F:/SDK/OpenCV/HiKivision/picture/lena.jpg");
    matchImage=imread("F:/SDK/OpenCV/HiKivision/picture/eye.png");

    int result_cols = srcImage.cols - matchImage.cols + 1;
    int result_rows = srcImage.rows - matchImage.rows + 1;
    result.create(result_cols, result_rows, CV_32FC1);

    matchTemplate(srcImage,matchImage,result,CV_TM_SQDIFF_NORMED);
    normalize(result, result, 0, 1, NORM_MINMAX, -1, Mat());

    double minVal = -1;
    double maxVal;
    Point minLoc;
    Point maxLoc;
    Point matchLoc;
    cout << "匹配度:" << minVal << endl;
    minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, Mat());


    cout << "匹配度:" << minVal << endl;

    matchLoc = minLoc;

    rectangle(srcImage, matchLoc, Point(matchLoc.x + matchImage.cols, matchLoc.y + matchImage.rows), Scalar(0, 255, 0), 2, 8, 0);

    imwrite("F:/SDK/OpenCV/HiKivision/picture/match.jpg",srcImage);
    return 0;
}

【数字图像处理】OpenCV3 扩展模块学习笔记

1. 特征检测方法

有许多用于特征检测和提取的算法,我们将会对其中大部分进行介绍。OpenCV最常使用的特征检测和提取算法有:

Harris:该算法用于检测角点;
SIFT:该算法用于检测斑点;
SURF:该算法用于检测角点;
FAST:该算法用于检测角点;
BRIEF:该算法用于检测斑点;
ORB:该算法代表带方向的FAST算法与具有旋转不变性的BRIEF算法;

通过以下方法进行特征匹配:

暴力(Brute-Force)匹配法;
基于FLANN匹配法;

可以采用单应性进行空间验证。

1.1. 特征定义

那么,究竟什么是特征呢?为什么一副图像的某个特定区域可以作为一个特征,而其他区域不能呢?粗略的讲,特征就是有意义的图像区域,该区域具有独特特征和易于识别性。因此角点及高密度区域都是很好的特征,而大量重复的模式或低密度区域(例如图像中的蓝色天空)则不是很好的特征。边缘可以将图像分为两个区域,因此也可以看做好的特征。斑点是与周围有很大差别的像素区域,也是有意义的特征。

大多数特征检测算法都会涉及图像的角点、边和斑点的识别,也有一些涉及脊向的概念,可以认为脊向是细长物体的对称轴,例如识别图像中的一条路。角点和边都好理解,那什么是斑点呢?斑点通常是指与周围有着颜色和灰度差别的区域。在实际地图中,往往存在着大量这样的斑点,如一颗树是一个斑点,一块草地是一个斑点,一栋房子也可以是一个斑点。由于斑点代表的是一个区域,相比单纯的角点,它的稳定性要好,抗噪声能力要强,所以它在图像配准上扮演了很重要的角色。

由于某些算法在识别和提取某类型特征的时候有较好的效果,所以知道输入图像是什么很重要,这样做有利于选择最合适的OpenCV工具包。

1.2. 角点检测原理

 角点在保留图像图形重要特征的同时,可以有效地减少信息的数据量,使其信息的含量很高,有效地提高了计算的速度,有利于图像的可靠匹配,使得实时处理成为可能。它的各种应用,这里我就不再赘述了。

1.2.1. Harris角点检测原理

我们先来看一幅图片,了解一下什么是角点?

上图中E,F中的角我们通常称作角点(corner points),他们具有以下特征:

  • 轮廓之间的交点;
  • 对于同一场景,即使视角发生变化,通常具备稳定性质的特征;
  • 该点附*区域的像素点无论在梯度方向上还是其梯度幅值上有着较大变化;

harris特征角最早在paper A Combined Corner and Edge Detector中被Chris Harris & Mike Stephens提出。

Harris角点检测的基本思想:算法基本思想是使用一个固定窗口在图像上进行任意方向上的滑动,比较滑动前与滑动后两种情况,窗口中的像素灰度变化程度,如果存在任意方向上的滑动,都有着较大灰度变化,那么我们可以认为该窗口中存在角点

1、灰度变化描述

当窗口发生[u,v]
移动时,那么滑动前与滑动后对应的窗口中的像素点灰度变化描述如下:

其中,u,v是窗口在水平,竖直方向的偏移,

图中蓝线圈出的部分我们称之为结构张量(structure tensor),用M表示。

    讲到这里,先明确一点,我们的目的是什么?我们的目的是寻找这样的像素点,它使得我们的u,v无论怎样取值,E(u,v)都是变化比较大的,这个像素点就是我们要找的角点。不难观察,上式近似处理后的E(u,v)是一个二次型,而由下述定理可知,

椭圆的长短轴是与结构张量M的两个特征值相对应的量。通过判断的情况我们就可以区分出‘flat’,‘edge’,‘corner’这三种区域,因为最直观的印象:

corner:在水平、竖直两个方向上变化均较大的点,即Ix、Iy都较大;
edge :仅在水平、或者仅在竖直方向有较大的点,即Ix和Iy只有其一较大 ;
flat : 在水平、竖直方向的变化量均较小的点,即Ix、Iy都较小;

而结构张量M是由Ix,Iy构成,它的特征值正好可以反映Ix,Iy的情况,下面我以一种更容易理解的方式来讲述椭圆的物理意义。

当然,大牛们并没有止步于此,这样通过判断两个变量的值来判断角点毕竟不是很方便。于是他们想出了一种更好的方法,对,就是定义角点响应函数R(corner response function),

至此,我们就可以通过判断R的值来判断某个点是不是角点了。

角点:R为大数值整数
边缘:R为大数值负数
平坦区:绝对值R是小数值

/*
@param src Input single-channel 8-bit or floating-point image.
@param dst Image to store the Harris detector responses. It has the type CV_32FC1 and the same
size as src .
@param blockSize Neighborhood size (see the details on #cornerEigenValsAndVecs ).
@param ksize Aperture parameter for the Sobel operator.
@param k Harris detector free parameter. See the formula above.
@param borderType Pixel extrapolation method. See #BorderTypes.
 */
CV_EXPORTS_W void cornerHarris( InputArray src, OutputArray dst, int blockSize,
                                int ksize, double k,
                                int borderType = BORDER_DEFAULT );
/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-02 23:45:24
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-05 07:27:48
 */
#include <opencv2/opencv.hpp>
using namespace cv;

int main()
{
    Mat srcImage,dstImage,normal_dst,norScale;
    srcImage=imread("F:/SDK/OpenCV/HiKivision/picture/chessboard.png");
    Mat out=srcImage.clone();
    cvtColor(srcImage,srcImage,CV_BGR2GRAY);
    //imshow("src",srcImage);

    dstImage=Mat::zeros(srcImage.size(),CV_32FC1);

    cornerHarris(srcImage,dstImage,2,3,0.04,BORDER_DEFAULT);
    normalize(dstImage,normal_dst,0,255,NORM_MINMAX,CV_32FC1,Mat());
    convertScaleAbs(normal_dst,norScale);

    for(int row=0;row<out.rows;row++){
        uchar* currentPtr=norScale.ptr(row);
        for(int col=0;col<out.cols;col++)
        {
            int value=(int)*currentPtr;//一般100-200之间 就是上面提到的角点响应函数R
            if(value>100)circle(out,Point(col,row),20,Scalar(0,0,255));
            currentPtr++;
        }
    }
    imwrite("F:/SDK/OpenCV/HiKivision/picture/out.jpg",out);
    imshow("out.jpg",out);
    waitKey(0);
    return 0;
}

1.2.2. Tomasi角点检测原理

R=min(Y1,Y2);

CV_EXPORTS_W void goodFeaturesToTrack( InputArray image, OutputArray corners,
                                     int maxCorners, double qualityLevel, double minDistance,
                                     InputArray mask = noArray(), int blockSize = 3,
                                     bool useHarrisDetector = false, double k = 0.04 );

下面都不讲原理,看不懂了

1.2.3. SURF特征检测

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-05 07:51:35
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-05 08:04:08
 */
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>

using namespace std;
using namespace cv;
using namespace cv::xfeatures2d;

int main()
{
    Mat srcImage = imread("F:/SDK/OpenCV/HiKivision/picture/board.jpg");
    //判断图像是否读取成功
    if (srcImage.empty())
    {
        cout << "图像加载失败" << endl;
        return -1;
    } 
    else
    {
        cout << "图像加载成功" << endl << endl;
    }
    namedWindow("原图像",WINDOW_AUTOSIZE);
    imshow("原图像",srcImage);
    Mat imageMid;   //定义滤波后图像
    int minHessian = 400;                           //定义Hessian矩阵阈值特征点检测算子
    Ptr<SURF> deteror=SURF::create(minHessian);

    vector<KeyPoint> keypoints;                     //定义KeyPoint类型的矢量容器vector存储检测到的特征点
    deteror->detect(srcImage,keypoints);            //调用detect检测特征点
    //绘制检测到的特征点
    Mat dstImage;
    drawKeypoints(srcImage, keypoints, dstImage, Scalar::all(-1), DrawMatchesFlags::DEFAULT);
    namedWindow("特征点检测",WINDOW_AUTOSIZE);
    imshow("特征点检测",dstImage);
    waitKey(0);
    return 0;
}

1.2.4. SIFT特征点检测

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-05 08:50:08
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-05 08:57:02
 */
#include "opencv2/opencv.hpp"
#include <opencv2/xfeatures2d.hpp>
#include <vector>
using namespace std;
using namespace cv;
using namespace cv::xfeatures2d;
int main()
{     
    Mat srcImg1 = imread("F:/SDK/OpenCV/HiKivision/picture/board.jpg");
    //Mat srcImg2 = imread("F:/SDK/OpenCV/HiKivision/picture/chessboard.png");
    //定义SIFT特征检测类对象
    Ptr<SIFT> deteror=SIFT::create();

    vector<KeyPoint>keyPoints1;
    vector<KeyPoint>keyPoints2;
    //特征点检测
    deteror->detect(srcImg1, keyPoints1);
    //deteror->detect(srcImg2, keyPoints2);
    //绘制特征点(关键点)
    Mat feature_pic1, feature_pic2;
    //drawKeypoints(srcImg2, keyPoints2, feature_pic2, Scalar(0, 0, 255));
    drawKeypoints(srcImg1, keyPoints1, feature_pic1, Scalar::all(-1), DrawMatchesFlags::DRAW_RICH_KEYPOINTS);//颜色随机,带有方向
    //显示原图
    imshow("src1", srcImg1);
    //imshow("src2", srcImg2);
    //显示结果
    imshow("feature1", feature_pic1);
    //imshow("feature2", feature_pic2);

    waitKey(0);
}

1.2.5. HOG 特征检测

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-05 09:00:19
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-05 09:09:55
 */
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace cv;
using namespace std;

int main(int argc, char** argv) 
{
    Mat src = imread("F:/SDK/OpenCV/HiKivision/picture/warker.png");
    if (src.empty()) 
    {
        cout << "Load image error..." << endl;
        return -1;
    }
    HOGDescriptor hog = HOGDescriptor();
    hog.setSVMDetector(hog.getDefaultPeopleDetector());

    vector<Rect> foundLocations;
    hog.detectMultiScale(src, foundLocations, 0, Size(8, 8), Size(32, 32), 1.05, 2);
    Mat result = src.clone();
    for (size_t t = 0; t < foundLocations.size(); t++) 
        rectangle(result, foundLocations[t], Scalar(0, 0, 255), 2, 8, 0);

    imwrite("hog.jpg",result);
    imshow("HOG SVM Detector Demo", result);

    waitKey();
    return 0;
}

1.2.6. LBP 特征提取

1.2.6.1. 原始LBP

原始LBP是在3*3的窗口内,以窗口中心元素为阈值,比较周围8个像素,若大于中心像素点,则标记为1,否则为0。然后这8个点就可以产生一个8位的二进制数(共有2^8=256种),转换为10进制数后,这个值就用来代替像素中心值。

1.2.6.2. 圆形LBP算子

由于原始LBP只是覆盖了一个固定半径范围内的小区域,不能满足不同尺寸和频率纹理的需要。对此,将3*3领域扩展到任意领域,用圆形领域代替正方形领域。

(1)将检测窗口划分为16*16的小区域(cell)。
(2)对于每个cell中的一个像素,计算得到它的LBP值。
(3)计算每个cell的直方图,即每个LBP值出现的频率,然后归一化。
(4)统计每个cell的直方图,连接成一个向量。

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-05 07:51:35
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-05 09:59:42
 */
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>

using namespace std;
using namespace cv;
using namespace cv::xfeatures2d;

int main()
{
    Mat srcImage = imread("F:/SDK/OpenCV/HiKivision/picture/board.jpg");
    Mat gray_src;

    namedWindow("原图像",WINDOW_AUTOSIZE);
    imshow("原图像",srcImage);
    cvtColor(srcImage,gray_src,CV_BGR2GRAY);
    int width=gray_src.cols;
    int heights=gray_src.rows;

    Mat dst =Mat::zeros(heights-2,width-2,CV_8UC1);
    for(int row =1;row<heights-1;row++){
        for(int col=1;col<width-1;col++)
        {
            uchar c=gray_src.at<uchar>(row,col);
            uchar code=0;
            code|=(gray_src.at<uchar>(row-1,col-1)>c)<<7;
            code|=(gray_src.at<uchar>(row-1,col)>c)<<6;
            code|=(gray_src.at<uchar>(row-1,col+1)>c)<<5;
            code|=(gray_src.at<uchar>(row,col-1)>c)<<4;
            code|=(gray_src.at<uchar>(row,col+1)>c)<<3;
            code|=(gray_src.at<uchar>(row+1,col-1)>c)<<2;
            code|=(gray_src.at<uchar>(row+1,col)>c)<<1;
            code|=(gray_src.at<uchar>(row+1,col+1)>c)<<0;
            dst.at<uchar>(row-1,col-1)=code;
        }
    }

    imshow("dst",dst);

    waitKey(0);

    return 0;
}

1.3. HAAR特征提取

1.3.1. 1.什么是haar特征?

特征 = 某个区域的像素点经过某种四则运算之后得到的结果。

这个结果可以是一个具体的值也可以是一个向量,矩阵,多维。实际上就是矩阵运算

1.3.2. 2.如何利用特征 区分目标?

阈值判决,如果大于某个阈值,认为是目标。小于某个阈值认为是非目标。

1.3.3. 3.如何得到这个判决?

使用机器学习,我们可以得到这个判决门限
Haar特征的计算原理

这些是在opencv中使用的haar特征。基础类型(5种) 核心(3种)all(6种),这里的14个图片分别对应十四种特征。

如何使用呢?

蓝色区域表明我们所得到的图片。黑白矩形框是我们的特征模板。

比如: 我们的模板是一个(10,10)的矩阵,共覆盖了100个像素点。黑白各占50个像素点

将当前模板放在图像上的任意位置上,在这里,用白色区域覆盖的50个像素之和减去黑色区域的50个像素之和得到我们的特征。

推导公式的一样性。

公式二:

这里的整个区域指黑白两色覆盖的整个100个像素。这两个权重是不一样的。

整体的权重值为1 黑色部分权重值为-2

整个区域的像素值 * 权重1 + 黑色部分 * 权重2
= 整个区域 * 1 + 黑色部分 * -2
=(黑 + 白) * 1 + 黑色部分 * -2
= 白色 – 黑色
Haar特征遍历

计算整幅图的Haar特征

如果想要计算整幅图的Haar特征,我们就需要遍历,

假设这个haar特征的模板是(10,10)共100个像素。图片的大小是(100,100)

如果想获取这个图片上所有的harr特征。使用当前模板沿着水平和竖直方向进行滑动。从上到下,从左到右进行遍历。

遍历的过程中还要考虑步长问题。这个模板一次滑动几个像素。一次滑动10个像素,就需要9次。加上最开始的第0次。

10个模板。

计算这100个模板才能将haar特征计算完毕。

如果我们的步长设置为5.那么就要滑动20次。400个模板。运算量增加4倍。

其实仅仅这样的一次滑动并没有结束,对于每一个模板还要进行几级缩放,才能完成。

需要三个条件:100100,1010,step=10,模板 1

模板 :缩放,这就需要再次进行遍历

运算量太大 :

计算量 = 14个模版20级缩放(1080/2*720/2) * (100点+ -) = 50-100亿
(50-100)*15 = 1000亿次
积分图

特征:计算矩形方块中的像素

有ABCD四个区域,每个区域都是一个矩形方块。

A区域是左上角那一块小区域,而B区域是包含A区域的长条。

C区域又是包含A的竖直长条。D区域是四个方块之和。

1,2,3,4表明这四个小区域。

进行快速计算:

A 1 B 1 2 C 1 3 D 1 2 3 4
4 = A-B-C+D = 1+1+2+3+4-1-2-1-3 = 4(10*10 变成3次+-)

任意一个方框,可由周围的矩形进行相减得到。

问题: 在计算每一个方块之前,需要将图片上所有的像素全部遍历一次。至少一次。

特征=(p1-p2-p3+p4)*w

p1 p2 p3 p4 分别是指某一个特征相邻的abcd的模块指针。

adaboost分类器

haar特征一般会和adaboost一起使用

haar特征 + Adaboost是一个非常常见的组合,在人脸识别上取的非常大成功

Adaboost分类器优点在于前一个基本分类器分出的样本,在下一个分类器中会得到加权。加权后的全体样本再次被用于训练下一个基本分类器。

就是说它可以加强负样本。

例如:我们有 苹果 苹果 苹果 香蕉 找出苹果

第一次训练权值分别为: 0.1 0.1 0.1 0.5

因为香蕉是我们不需要的所以0.5,我们正确的样本系数减小,负样本得到加强。

把出错的样本进行加强权值。再把整个结果送到下一个基本分类器

训练终止条件:

  1. for迭代的最大次数 count
  2. 每次迭代完的检测概率p(p是最小的检测概率,当大于p就终止)

分类器的结构

haar特征计算完之后要进行阈值判决,实际是一个个判决过程。

if(haar> T1) 苹果[第一级分类器];[第二级分类器]and haar>T2

两级都达成了,才会被我们认定为苹果。

两级分类器的阈值分别是t1 和 t2,对于每一级的分类器我们称之为强分类器。

2个强分类器组成。一般有15-20强分类器。 要连续满足15-20个强分类器才能认证为目标。

3个强分类器,每个强分类器会计算出一个独立的特征点。使用每一个独立的特征进行阈值判。

强分类器1 特征x1 阈值t1 强分类器2 特征x2 阈值t2 3同理(阈值是通过训练终止时得到的)

进行判决过程: x1>t1 and x2>t2 and x3>t3

三个判决同时达成,目标-》苹果

三个强分类器只要有一个没通过就会被判定为非苹果。作用:判决。而弱分类器作用:计算强分类器特征x1 x2 x3

每个强分类器由若干个弱分类器组成

1强 对多个弱分类器 对应多个特征节点

弱分类器作用:计算强分类器特征x1 x2 x3

强分类器的输入特征是多个弱分类器输出特征的综合处理。

x2 = sum(y1,y2,y3)

y1 弱分类器特征谁来计算的?

node节点来计算

一个弱分类器最多支持三个haar特征,每个haar特征构成一个node节点

Adaboost分类器计算过程

3个haar -》 3个node

node1 haar1 > node的阈值T1 z1 = a1
node1 haar1 < node的阈值T1 z1 = a2

Z = sum(z1,z2,z3) > 弱分类器T y1 = AA
Z = sum(z1,z2,z3) < 弱分类器T y1 = BB

从node向强分类器

第一层分类器:haar->Node z1 z2 z3 得到的就是z1 z2 z3

第二层分类器:Z>T y1 y2 y3: 弱分类器的计算特征

第三层分类器:x = sum(y1,y2,y3) > T1 obj

训练过程

1.初始化数据 权值分布 苹果 苹果 苹果 香蕉 0.1 0.1 0.1 0.1 (第一次权值都是相等的)

2.遍历阈值 p计算出误差概率,找到最小的minP t

3.计算出G1(x)

4.权值分布 update更新权重分布: 0.2 0.2 0.2 0.7

5.训练终止条件。

1.3.4. 基于Haar特征与级联分类器的人脸检测

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-05 11:19:53
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-05 11:23:38
 */
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

String fileName = "/home/pi/Desktop/WorkSpace/Study/data/haarcascade_frontalface_default.xml";//当前项目文件下的xml文件//opencv自带训练好的人脸识别级联器
CascadeClassifier face_cascade;//定义用来做目标检测的级联分类器的一个类

int main(int argc, char** argv)
{
    VideoCapture cap(0);
    if (!face_cascade.load(fileName)) //检测级联器是否加载成功
    {
        printf("could not load face feature data.../n");
        return -1;
    }

    Mat src,gray;
    namedWindow("src", WINDOW_AUTOSIZE);
    while(true){
        cap>>src;
        cvtColor(src, gray, COLOR_BGR2GRAY);//转换为灰度图
        //直方图均衡化
        equalizeHist(gray, gray);//因为积分图像特征基于矩形区域的差,如果直方图是不平衡的,这些差异就有可能由于整体光照或者测试图像的曝光而倾斜,所以这一步非常重要

        vector<Rect> faces;
        //利用detectMultiScale搜索图像
        face_cascade.detectMultiScale(gray, //灰度图像
                                    faces, //vector<Rect>边界矩形
                                    1.1,//scaleFactor表示每次图像尺寸减小的比例
                                    3, //minNeighbors表示每一个目标至少要被检测到3次才算是真的目标(因为周围的像素和不同的窗口大小都可以检测到人脸)
                                    0, //flags 旧版本OpenCV 1.x级联工具
                                    Size(30, 30)); //目标的最小尺寸
        for (size_t i = 0; i < faces.size(); i++)
        {
            //检索到人脸则画圆形
            Point center(faces[i].x + faces[i].width*0.5, faces[i].y + faces[i].height*0.5);
            ellipse(src, center, Size(faces[i].width*0.5, faces[i].height*0.5), 0, 0, 360, Scalar(0,255,255), 4, 8, 0);
        }
        imshow("Face_detect", src);
        waitKey(30);
    }
    return 0;
}

1.3.5. 基于Haar特征的模型训练与识别

关键在于数据的采集与标记,这是一个非常麻烦的过程

给一个狗脸识别的例子
链接:https://pan.baidu.com/s/1d2eThpD0bbU0ZnZeV5XZAA
提取码:06cv

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-05 11:19:53
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-05 11:23:38
 */
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

String fileName = "F:/SDK/OpenCV/HiKivision/data/Train/haarresult/cascase.xml";//当前项目文件下的xml文件//opencv自带训练好的人脸识别级联器
CascadeClassifier face_cascade;//定义用来做目标检测的级联分类器的一个类

int main(int argc, char** argv)
{
    VideoCapture cap(0);
    if (!face_cascade.load(fileName)) //检测级联器是否加载成功
    {
        printf("could not load face feature data.../n");
        return -1;
    }

    Mat src,gray;
    namedWindow("src", WINDOW_AUTOSIZE);
    while(true){
        cap>>src;
        cvtColor(src, gray, COLOR_BGR2GRAY);//转换为灰度图
        //直方图均衡化
        equalizeHist(gray, gray);//因为积分图像特征基于矩形区域的差,如果直方图是不平衡的,这些差异就有可能由于整体光照或者测试图像的曝光而倾斜,所以这一步非常重要

        vector<Rect> faces;
        //利用detectMultiScale搜索图像
        face_cascade.detectMultiScale(gray, //灰度图像
                                    faces, //vector<Rect>边界矩形
                                    1.1,//scaleFactor表示每次图像尺寸减小的比例
                                    3, //minNeighbors表示每一个目标至少要被检测到3次才算是真的目标(因为周围的像素和不同的窗口大小都可以检测到人脸)
                                    0, //flags 旧版本OpenCV 1.x级联工具
                                    Size(30, 30)); //目标的最小尺寸
        for (size_t i = 0; i < faces.size(); i++)
        {
            //检索到人脸则画圆形
            Point center(faces[i].x + faces[i].width*0.5, faces[i].y + faces[i].height*0.5);
            ellipse(src, center, Size(faces[i].width*0.5, faces[i].height*0.5), 0, 0, 360, Scalar(0,255,255), 4, 8, 0);
        }
        imshow("Face_detect", src);
        waitKey(30);
    }
    return 0;
}

1.4. 特征匹配

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2021-05-05 07:51:35
 * @LastEditors: Yueyang
 * @LastEditTime: 2021-05-05 12:56:57
 */
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>

using namespace std;
using namespace cv;
using namespace cv::xfeatures2d;

int main()
{
    Mat srcImage1 = imread("/home/pi/Desktop/WorkSpace/OpenCV/picture/pic1.jpg");//OBJ
    Mat srcImage2 = imread("/home/pi/Desktop/WorkSpace/OpenCV/picture/pic2.jpg");//SCENE

    if(srcImage1.channels()>1)
    cvtColor(srcImage1,srcImage1,COLOR_BGR2GRAY);
    if(srcImage2.channels()>1)
    cvtColor(srcImage2,srcImage2,COLOR_BGR2GRAY);

    int minHessian = 400;                           //定义Hessian矩阵阈值特征点检测算子
    Ptr<SURF> deteror=SURF::create(minHessian);

    vector<KeyPoint> keypoints1;                     //定义KeyPoint类型的矢量容器vector存储检测到的特征点
    vector<KeyPoint> keypoints2;  

    Mat descrp1,descrp2;
    deteror->detectAndCompute(srcImage1,Mat(),keypoints1,descrp1);            //调用detect检测特征点
    deteror->detectAndCompute(srcImage2,Mat(),keypoints2,descrp2);            //调用detect检测特征点


    FlannBasedMatcher matcher;
    vector<DMatch>mathces;
    matcher.match(descrp1,descrp2,mathces);

    double minDist=1000;
    double maxDist=0;

    for(int i=0;i<descrp1.rows;i++)
    {
        double dist=mathces[i].distance;
        if(dist>maxDist)maxDist=dist;
        if(dist<minDist)minDist=dist;
    }

    printf("min %f,max %f",minDist,maxDist);


    vector<DMatch>goodmach;
    for(int i=0;i<descrp1.rows;i++)
    {
        double dist =mathces[i].distance;
        if(dist<max(2*minDist,0.02))
        {
            goodmach.push_back(mathces[i]);
        }
    }


    Mat dst;
    drawMatches(srcImage1,keypoints1,srcImage2,keypoints2,goodmach,dst);

    vector<Point2f>obj;
    vector<Point2f>scene;

    for(int t=0;t<goodmach.size();t++)
    {
        obj.push_back(keypoints1[goodmach[t].queryIdx].pt);
        scene.push_back(keypoints2[goodmach[t].trainIdx].pt);   
    }
    Mat H=findHomography(obj,scene,RANSAC);
    vector<Point2f>obj_coners(4);
    vector<Point2f>scene_coners(4);
    obj_coners[0]=Point(0,0);
    obj_coners[1]=Point(srcImage1.cols,0);
    obj_coners[2]=Point(srcImage1.cols,srcImage1.rows);
    obj_coners[3]=Point(0,srcImage1.rows);

    perspectiveTransform(obj_coners,scene_coners,H);
    line(dst,scene_coners[0]+Point2f(srcImage1.cols,0),scene_coners[1]+Point2f(srcImage1.cols,0),Scalar(0,0,255),2,8,0);
    line(dst,scene_coners[1]+Point2f(srcImage1.cols,0),scene_coners[2]+Point2f(srcImage1.cols,0),Scalar(0,0,255),2,8,0);
    line(dst,scene_coners[2]+Point2f(srcImage1.cols,0),scene_coners[3]+Point2f(srcImage1.cols,0),Scalar(0,0,255),2,8,0);
    line(dst,scene_coners[3]+Point2f(srcImage1.cols,0),scene_coners[0]+Point2f(srcImage1.cols,0),Scalar(0,0,255),2,8,0);



    imshow("match",dst);

    waitKey(0);

    return 0;
}