步進電機Linux驅動

tstars發表於2024-06-17

本文將介紹步進電機Linux驅動程式,分為以三部分:步進電機介紹,硬體原理圖以及程式編寫
1 步進電機介紹
步進電機是一種將電脈衝訊號轉變為角位移或者線位移的開環控制元件,在非超載的狀態下,電機的轉速、停止位置只取決於脈衝訊號的頻率和脈衝數,不受負載變化的影響,並且只有週期性誤差而沒有累積誤差,因此在速度、位置等控制領域有廣泛的應用。
步進電機主要分為三類:永磁式,反應式,混合式
主要技術指標如下:


下圖是一個四相永磁式步進電機,以它為例介紹步進電機是如何工作的。內容參考韋東山imx6ull裸機開發手冊。


更具體地內容可以參考韋東山老師地驅動實驗班課程影片。

2 硬體原理圖
我使用的步進電機與老師課程上所使用的型號不同,但是使用原理上是一致的。我所使用的電機是四線雙極性步進電機,使用的驅動晶片是TC1508S,外部電路原理圖如下:

分析上述原理圖,驅動模組接受4個GPIO輸出,得到四個輸出。使用開發板上imx6ull的四個引腳:GPIO1_IO01,GPIO1_IO02, JTAG_MOD,GPIO1_IO04,分別接到模組的IA,IB,IC,ID,模組的四個輸出OA,OB,OC,OD分別接到電機的A+,A-,B+,B-,模組的輸入和輸出對應關係如下圖:

根據邏輯真值表,可以得到四線雙極性步進電機對應imx6ull引腳的輸出真值表如下圖:

3 程式
3.1 裝置樹節點

點選檢視程式碼
mymotor {
			compatible = "motor_driver";
			pinctrl-names = "default";
			pinctrl-0 = <&pinctrl_motor>;
			motor-gpios = <
							&gpio1 1 GPIO_ACTIVE_HIGH
							&gpio1 2 GPIO_ACTIVE_HIGH
							&gpio1 10 GPIO_ACTIVE_HIGH
							&gpio1 4 GPIO_ACTIVE_HIGH
							&gpio1 3 GPIO_ACTIVE_LOW	/*LED*/
							>;
			
			status = "okay";
		};


pinctrl_motor: motorGrp {                /*!< Function assigned for the core: Cortex-A7[ca7] */
            fsl,pins = <
                MX6UL_PAD_GPIO1_IO01__GPIO1_IO01           0x000010B0 /*A*/
				MX6UL_PAD_GPIO1_IO02__GPIO1_IO02           0x000010B0 /*B*/
				MX6UL_PAD_JTAG_MOD__GPIO1_IO10             0x0000B0A0 /*C*/
                MX6UL_PAD_GPIO1_IO04__GPIO1_IO04           0x000010B0 /*D*/
				MX6UL_PAD_GPIO1_IO03__GPIO1_IO03		   0x000010B0 /*LED*/
            >;
        };

3.2 驅動程式程式碼
驅動程式提供三種驅動方式,具體使用哪種由應用程式決定,驅動程式碼如下:

點選檢視程式碼
#include "asm-generic/current.h"
#include "asm-generic/errno-base.h"
#include "asm-generic/poll.h"
#include "asm-generic/siginfo.h"
#include "asm/signal.h"
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/gpio/driver.h"
#include "linux/irqreturn.h"
#include "linux/kdev_t.h"
#include "linux/nfs_fs.h"
#include "linux/of.h"
#include "linux/wait.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/ktime.h>
#include <linux/delay.h>
#include <linux/fcntl.h>
#include <linux/timer.h>
#include <linux/workqueue.h>
#include <asm/current.h>

#define clock   1
#define unclock 0

static int major;   //主裝置號
static struct class *motor_class;
static int count;   //引腳數量,驅動步進電機,應該是4個引腳

struct gpio_struct{
    int gpio_num;   //GPIO引腳編號,老版本
    struct gpio_desc *desc; //gpio引進描述結構體
    int irq;    //中斷號
    int flag;
};

struct motor_struct {
    int angle_per_beat; //每一節拍轉動的角度
    int beats_per_round;  //一輪節拍數量
    char *name; //勵磁方式名稱
    int *beats;  //節拍陣列
};

static int beats_1phase[4] = {0xD, 0x7, 0xE, 0xB}; //一相勵磁方式,每一次旋轉1.8度.實測每一步是18度
static int beats_2phase[4] = {0x5, 0x6, 0xA, 0x9}; //二相勵磁方式,每一次旋轉18度
static int beats_12pahse[8] = {0xD, 0x5, 0x7, 0x6, 0xE, 0xA, 0xB, 0x9}; //一二相勵磁方式,每次旋轉9度

static struct motor_struct motor_1phase = {
    .angle_per_beat = 18,
    .beats_per_round = 4,
    .name = "1phase",
    .beats = beats_1phase,
};

static struct motor_struct motor_2phase = {
    .angle_per_beat = 18,
    .beats_per_round = 4,
    .name = "2phase",
    .beats = beats_2phase,
};

static struct motor_struct motor_12phase = {
    .angle_per_beat = 9,
    .beats_per_round = 8,
    .name = "12phase",
    .beats = beats_12pahse,
};


struct gpio_struct *motor_gpios;    //步進電機驅動晶片的四個輸入GPIO

static void delay_us(unsigned int us)
{
    /*延時us = 1時延時1us函式,因為直接用核心函式好像不太準,所以使用ktime_get_ns函式*/
    u64 time = ktime_get_ns();
	while (ktime_get_ns() - time < us*1000);
}

static void delay_ms(unsigned int ms)
{
    /*延時ms = 1時延時1ms函式*/
    while(ms--)
    {
        delay_us(1000); //延時1ms
    }
}

int motor_open(struct inode *node, struct file *file)
{
    int i;
    for(i = 0; i < count; i++)
    {
        gpiod_direction_output(motor_gpios[i].desc, 0);
    }
    return 0;
}

void take_a_step(struct motor_struct *motor, int cur_step)
{
    /*設定一個節拍
    分別設定電機四個引腳的電平狀態
    */
    int i;
    for(i = 0; i < 4; i++)
    {
        gpiod_set_value(motor_gpios[i].desc, (motor->beats[cur_step] >> i) & 1);
    }
    // printk("cur_steps: %d, beat: %0x\n", cur_step, motor->beats[cur_step]);
}

void take_steps(struct motor_struct *motor, int *run_param)
{
    /*驅動電機執行
    (1)motor是使用的電機驅動方式
    (2)run_param是控制電機執行速度和轉動的角度
    */
    int i;
    int steps;
    int speed;
    int led_state = 0;
    int direction = motor->beats_per_round - 1;  //用於調整正轉/反轉
    /*轉/s,那麼每秒轉動360 * run_param[0] 度
    1s需要360 * run_param[0] / motor->angle_per_beat這麼多拍
    那麼每拍間隔 1000 / (360 * run_param[0] / motor->angle_per_beat) ms
    */
    speed = 1000 / (360 * run_param[0] / motor->angle_per_beat);   //執行速度其實也就是延時的時間,單位ms,上限每秒25轉,也即每拍間隔至少1ms
    printk("speed: %d, angle: %d, choice: %d\n", run_param[0], run_param[1], run_param[2]);
    steps = run_param[1] / motor->angle_per_beat;  //需要的步數 
    // printk("steps: %d\n", steps);
    for(i = 0; i < steps; i++)
    {
        if(run_param[3])
        {
            /*正轉*/
            take_a_step(motor, i % motor->beats_per_round); //走一步

        }
        else 
        {
            /*反轉*/
            take_a_step(motor, direction - i % motor->beats_per_round); //走一步
        }
        led_state ^= 1;
        gpiod_set_value(motor_gpios[count-1].desc, led_state);
        delay_ms(speed);
    }
}

ssize_t motor_write(struct file *file, const char __user *user_buf, size_t size, loff_t *offset)
{
    /*使用最簡單的四節拍方式
    kernel_buf[0] 速度,轉/s,需要轉換為延時時間,上限25轉/s,也即每一拍至少間隔1ms
    kernel_buf[1] 角度
    kernel_buf[2] 驅動方式
    kernel_buf[3] 方向
    */
    int kernel_buf[4];
    int err;
    int choice; //選擇驅動方式,1,表示1相,2表示2相,3表示12相
    err = copy_from_user(kernel_buf, user_buf, size);
    if(kernel_buf[0] >= 25 || kernel_buf[0] <= 0)
    {
        /*限位保護*/
        printk("speed is limited between 0 and 25.\n");
        return -1;
    }
    // printk("delay time: %d\n", kernel_buf[0]);
    choice = kernel_buf[2];
    // take_steps(&motor_1phase, kernel_buf);
    switch (choice) {
    case 1 : take_steps(&motor_1phase, kernel_buf); break;
    case 2 : take_steps(&motor_2phase, kernel_buf); break;
    case 3 : take_steps(&motor_12phase, kernel_buf); break;
    default: printk("motor choice input err!, please input 1, 2, or 3\n"); return -1;
    }

    return size;
}

static struct file_operations motor_opr = {
    .owner = THIS_MODULE,
    .write = motor_write,
    .open = motor_open
};

int motor_probe(struct platform_device *pdev)
{
    /*
    驅動和裝置節點匹配時執行
    在這裡會做一些初始化操作:獲取裝置樹資訊,註冊驅動file_operation結構體
    */
    struct device_node *node = pdev->dev.of_node;   //獲取裝置樹中引腳對應的device_node結構體
    int i;
    enum of_gpio_flags flag;

    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    count = of_gpio_named_count(node, "motor-gpios"); //獲取裝置樹節點中定義了多少個GPIO
    printk("counts: %d\n", count);
    if(count <= 0)
    {
        printk("%s %s line %d, No available gpio, count = %d\n", __FILE__, __FUNCTION__, __LINE__, count);
        return -1;
    }
    motor_gpios = kzalloc(sizeof(struct gpio_struct) * count, GFP_KERNEL);  //為自定義的引腳結構體分配記憶體
    // //獲取裝置樹中定義的節點資訊
    for(i = 0; i < count; i++)
    {
    //     //獲取引腳編號
        motor_gpios[i].gpio_num = of_get_named_gpio_flags(node, "motor-gpios", i, &flag);
        printk("gpio_num: %d, value: %d\n", motor_gpios[i].gpio_num, gpio_get_value(motor_gpios[i].gpio_num));
        if(motor_gpios[i].gpio_num < 0)
        {
            printk("%s %s line %d, of_get_gpio_flags failed\n", __FILE__, __FUNCTION__, __LINE__);
            return -1;
        }
        //獲取引腳描述結構體
        motor_gpios[i].desc = gpiod_get_index(&pdev->dev, "motor", i);   //這裡使用get,那麼在remove函式里就需要使用put釋放引腳
        motor_gpios[i].flag = flag & OF_GPIO_ACTIVE_LOW;   
        motor_gpios[i].irq = gpiod_to_irq(motor_gpios[i].desc);   //獲取對應的中斷號

    }

    /*註冊file_operaton結構體,建立類和裝置*/
    major = register_chrdev(0, "motor_driver", &motor_opr);
    motor_class = class_create(THIS_MODULE, "motor_class");
    if(IS_ERR(motor_class))
    {
        printk("%s %s line %d, class create failed\n", __FILE__, __FUNCTION__, __LINE__);
        unregister_chrdev(major, "motor_driver");
        return PTR_ERR(motor_class);
    }
    //建立裝置節點,不需要為每個引腳都建立裝置節點,所有引腳(按鍵)共用一個裝置節點
    device_create(motor_class, NULL, MKDEV(major, 0), NULL, "motor");

    return 0;
}

int motor_remove(struct platform_device *pdev)
{
    int i;
    /*解除安裝驅動時執行,在這裡要撤銷中斷,登出驅動、類和裝置節點,釋放動態分配的記憶體*/
    //刪除裝置節點
    device_destroy(motor_class, MKDEV(major, 0));
    //刪除類
    class_destroy(motor_class);
    //撤銷驅動
    unregister_chrdev(major, "motor_driver");
    //撤銷中斷,釋放GPIO
    for(i = 0; i < count; i++)
    {
        gpiod_put(motor_gpios[i].desc);
    }
    kfree(motor_gpios);   //釋放記憶體
    return 0;
}

static const struct of_device_id motors_match_table[] = {
    {
        .compatible = "motor_driver"
    }
};

struct platform_driver motor_driver = {
    .probe = motor_probe,
    .remove = motor_remove,
    .driver = {
        .name = "mymotor_driver",
        .of_match_table = motors_match_table,
    },
};


static int __init motor_drv_init(void)
{
    int err;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    err = platform_driver_register(&motor_driver);
    return err;
}

static void __exit motor_drv_exit(void)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    platform_driver_unregister(&motor_driver);
}

module_init(motor_drv_init);
module_exit(motor_drv_exit);
MODULE_LICENSE("GPL");
3.3 應用程式程式碼 應用程式程式碼如下:
點選檢視程式碼
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>

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

    int fd;
    int buf[4]; //buf[0]表示速度,單位轉/s,上限25,buf[1]表示轉動的角度, buf[3]表示使用的驅動方式,1表示1相,2表示2相,3表示12相
    if(argc != 6)
    {
        printf("Usage: %s <key_dev> <speed> <angle> <choice> <dirction 1 or 0>\n", argv[0]);
        return -1;
    }

    buf[0] = atoi(argv[2]);
    buf[1] = atoi(argv[3]);
    buf[2] = atoi(argv[4]);
    if(strcmp(argv[5], "clock") == 0)
    {
        /*順時針*/
        buf[3] = 1;
    }
    else
    {
        buf[3] = 0;
    }

    fd = open(argv[1], O_RDWR);
    write(fd, buf, 16);

    return 0;
}

實物圖如下:

相關文章