Linux 驅動像微控制器一樣讀取一幀dmx512串列埠資料

2023-01-17 12:00:58

硬體全志R528

目標:實現Linux 讀取一幀dmx512串列埠資料。

問題分析:因為串列埠資料量太大,幀與幀之間的間隔太小。通過Linux自帶的讀取函數方法無法獲取到

幀頭和幀尾,讀取到的資料都是快取區中的,資料量又大。導致緩衝區中一直有很多資料,

又由於dmx512資料協定中並沒有幀頭幀尾欄位只有普通資料,無法通過特定的幀頭幀尾擷取到一完整幀的資料。

所以只能像微控制器一樣通過串列埠暫存器對LSR 的UART_LSR_FE位 (接收到錯誤幀)認為是一幀結束和開始。

通過對Linux驅動讀取串列埠資料的過程分析,

tty_read() ----> ld->ops->read() ----> n_tty_read()
n_tty_read()中add_wait_queue(&tty->read_wait, &wait)沒有資料的時候上層的read程序阻塞在此
而在串列埠有資料來的時候n_tty_receive_buf()--->wake_up_interruptible(&tty->read_wait),喚醒上面的read程序n_tty_read()中會繼續執行,將資料拷到使用者空間
從整個分析來看,uart驅動會把從硬體接受到的資料暫時存放在tty_buffer裡面,然後呼叫線路規程的receive_buf()把資料存放到tty->read_buf裡面,

而系統呼叫的read()函數直接從tty->read_buf裡面讀取資料。

所以最終判斷在uart的串列埠中斷接收處理常式中增加接收程式碼比較合適。

 

 Linux 設定非標準波特率參考上次的部落格。

方法:

1、寫一個簡單字元驅動dmx512_uart.c,放在sunxi-uart.c同資料夾中。

在驅動讀函數中設定全域性變數標識,等待讀取資料,後copy_to_user上傳到使用者空間.

修改同目錄下的Makefile 和Kconfig 後新增到核心,編譯到核心中。

 

/*dmx512_uart.c 程式碼*/
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include "dmx512_uart.h"

#define CDEV_NAME  "dmx512_uart_dev"
struct dmx512_uart_dev *dmx512_devp;


static ssize_t dmx512drv_read (struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
    int len =0;
    int num =0;
    int ret =0;
    int i=0;
    //printk("%s start\n",__func__);

    if(size > DMX512_BUF_LEN)
    {
        dmx512_devp->r_size = DMX512_BUF_LEN;
    }
    else
    {
        dmx512_devp->r_size = size;
    }
    memset(dmx512_devp->dmx_buff,0,sizeof(dmx512_devp->dmx_buff));
    dmx512_devp->end_read_flag = false;
    dmx512_devp->recv_len =0;
    dmx512_devp->num_break =0;
    dmx512_devp->start_read_flag = true;

    while(!dmx512_devp->end_read_flag) /*等待獲取資料*/
    {
        msleep(100);
        num++;
        if(num > 50)
        {
            printk("timeout\n");
            break;
        }
    }
    if(dmx512_devp->recv_len < size)
    {
        len = dmx512_devp->recv_len;
    }
    else
    {    
        len = size;    
    }
    
    if(copy_to_user(buf,dmx512_devp->dmx_buff, len))
        ret = -EFAULT;
    else{
        ret = len;
    }
    //printk("%s end\n",__func__);
    return ret;
    
}
static ssize_t dmx512drv_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{

    return 0;
}
static int dmx512drv_close (struct inode *inodp, struct file *filp)
{
    //printk("%s\n",__func__);
    return 0;

}
static int dmx512drv_open (struct inode *inodp, struct file *filp)
{
    //printk("%s\n",__func__);
    return 0;
}

static const struct file_operations dmx512drv_fops =
{
    .owner = THIS_MODULE,
    .open =dmx512drv_open,
    .read =dmx512drv_read,
    .write =dmx512drv_write,
    .release =dmx512drv_close,
};

static int __init dmx512_init(void)
{
    int ret;
    dmx512_devp =kzalloc(sizeof(struct dmx512_uart_dev), GFP_KERNEL);
    if(!dmx512_devp)
    {
        ret = -ENOMEM;
        return ret;
    }
#if 0    
    /*動態申請dev*/
    ret = alloc_chrdev_region(&dmx512_devp->dev,0, 1, CDEV_NAME);
    if(ret)
    {
        printk("failed to allocate char device region\n");
        return ret;
    }

    cdev_init(&dmx512_devp->cdev,&dmx512drv_fops);
    
    ret = cdev_add(&dmx512_devp->cdev,dmx512_devp->dev,1);    
    if(ret)
    {
        printk("failed to cdev_add\n");
        goto unregister_chrdev;

    }
        
    return 0;
unregister_chrdev:
    unregister_chrdev_region(dmx512_devp->dev,1);
    return ret;
#endif 
    dmx512_devp->dev_major = register_chrdev(0,"dmx512_uart_drv",&dmx512drv_fops);
    if(dmx512_devp->dev_major < 0)
    {
        printk(KERN_ERR"register_chrdev error\n");
        ret =- ENODEV;
        goto err_0;

    }
    dmx512_devp->cls = class_create(THIS_MODULE,"dmx512_cls");
    if(IS_ERR(dmx512_devp->cls))
    {
        printk(KERN_ERR"class_create error\n");
        ret = PTR_ERR(dmx512_devp->cls);
        goto err_1;
    }
    dmx512_devp->dev = device_create(dmx512_devp->cls, NULL,MKDEV(dmx512_devp->dev_major, 0),NULL,"dmx512_uart");
    if(IS_ERR(dmx512_devp->dev))
    {
        printk(KERN_ERR"device_create error\n");
        ret = PTR_ERR(dmx512_devp->dev);
        goto err_2;
    }
    return 0;

err_2:
        class_destroy(dmx512_devp->cls);
err_1:
        unregister_chrdev(dmx512_devp->dev_major,"dmx512_uart_drv");
            
err_0:
    kfree(dmx512_devp);
    return ret;

}

static void __exit  dmx512_exit(void)
{
#if 0
    cdev_del(&dmx512_devp->cdev);
    unregister_chrdev_region(dmx512_devp->dev,1);
#endif
    device_destroy(dmx512_devp->cls, MKDEV(dmx512_devp->dev_major, 0));
    class_destroy(dmx512_devp->cls);
    unregister_chrdev(dmx512_devp->dev_major,"dmx512_uart_drv");
    kfree(dmx512_devp);

}


module_init(dmx512_init);
module_exit(dmx512_exit);
MODULE_LICENSE("GPL");


/*dmx512_uart.h 標頭檔案*/
#ifndef _DMX512_UART_H_
#define _DMX512_UART_H_

#define DMX512_BUF_LEN (4096+1+3)
struct dmx512_uart_dev
{
    unsigned int dev_major;
    struct class *cls;
    struct device *dev;
    int recv_len;
    int r_size;
    bool start_read_flag;
    bool end_read_flag;
    unsigned char num_break;
    unsigned char dmx_buff[DMX512_BUF_LEN];
};

extern struct dmx512_uart_dev *dmx512_devp;

#endif /*_DMX512_UART_H_*/

 

2、串列埠接收中斷處理常式中根據全域性變數標識開始讀取資料。

通過對暫存器LSR 的UART_LSR_FE位進行判斷,為新的一幀的開始和結束。

通過對核心原始碼的分析找到uart的串列埠中斷接收處理常式。在

sunxi-uart.c -》static unsigned int sw_uart_handle_rx(struct sw_uart_port *sw_uport, unsigned int lsr)

static unsigned int sw_uart_handle_rx(struct sw_uart_port *sw_uport, unsigned int lsr)
{
    unsigned char ch = 0;
    int max_count = 256;
    char flag;

#if IS_ENABLED(CONFIG_SERIAL_SUNXI_DMA)
    if ((sw_uport->dma->use_dma & RX_DMA)) {
        if (lsr & SUNXI_UART_LSR_RXFIFOE) {
            dev_info(sw_uport->port.dev, "error:lsr=0x%x\n", lsr);
            lsr = serial_in(&sw_uport->port, SUNXI_UART_LSR);
            return lsr;
        }
    }
#endif

    if(lsr & SUNXI_UART_LSR_FE)
    {
        if((dmx512_devp->start_read_flag) && (strncmp(sw_uport->name,"uart1",5) ==0))  /*現在用的是uart1 不同的埠需要調整,也可以通過驅動直接傳過來*/
        {
            dmx512_devp->num_break++;
            if(dmx512_devp->num_break ==1)
                dmx512_devp->recv_len =0;
        }
    }
    do {

        if((dmx512_devp->start_read_flag) && (strncmp(sw_uport->name,"uart1",5) ==0))
        {
                if((lsr & SUNXI_UART_LSR_FE) &&(max_count !=256))
                        dmx512_devp->num_break++;    
        }
        
        if (likely(lsr & SUNXI_UART_LSR_DR)) {
            ch = serial_in(&sw_uport->port, SUNXI_UART_RBR);
#if IS_ENABLED(CONFIG_SW_UART_DUMP_DATA)
            sw_uport->dump_buff[sw_uport->dump_len++] = ch;
#endif
        } else
            ch = 0;

        flag = TTY_NORMAL;
        sw_uport->port.icount.rx++;
        if (unlikely(lsr & SUNXI_UART_LSR_BRK_ERROR_BITS)) {
            /*
             * For statistics only
             */
            if (lsr & SUNXI_UART_LSR_BI) {
                lsr &= ~(SUNXI_UART_LSR_FE | SUNXI_UART_LSR_PE);
                sw_uport->port.icount.brk++;

                /*
                 * We do the SysRQ and SAK checking
                 * here because otherwise the break
                 * may get masked by ignore_status_mask
                 * or read_status_mask.
                 */
                if (!ch && uart_handle_break(&sw_uport->port))
                    goto ignore_char;
            } else if (lsr & SUNXI_UART_LSR_PE)
                sw_uport->port.icount.parity++;
            else if (lsr & SUNXI_UART_LSR_FE)
                sw_uport->port.icount.frame++;
            if (lsr & SUNXI_UART_LSR_OE)
                sw_uport->port.icount.overrun++;

            /*
             * Mask off conditions which should be ignored.
             */
            lsr &= sw_uport->port.read_status_mask;
#if IS_ENABLED(CONFIG_SERIAL_SUNXI_CONSOLE)
            if (sw_is_console_port(&sw_uport->port)) {
                /* Recover the break flag from console xmit */
                lsr |= sw_uport->lsr_break_flag;
            }
#endif
            if (lsr & SUNXI_UART_LSR_BI)
                flag = TTY_BREAK;
            else if (lsr & SUNXI_UART_LSR_PE)
                flag = TTY_PARITY;
            else if (lsr & SUNXI_UART_LSR_FE)
                flag = TTY_FRAME;
        }
        if (uart_handle_sysrq_char(&sw_uport->port, ch))
            goto ignore_char;
        
        //printk("sw_uport->name =%s\n",sw_uport->name);
        /*增加對break的判斷*/
  
        if((dmx512_devp->start_read_flag) && (strncmp(sw_uport->name,"uart1",5) ==0))
        {    
            if(dmx512_devp->num_break ==1)
            {
                dmx512_devp->dmx_buff[dmx512_devp->recv_len] =ch;
                dmx512_devp->recv_len++;
                if(dmx512_devp->recv_len >= dmx512_devp->r_size)
                {
                    dmx512_devp->start_read_flag = false;
                    dmx512_devp->end_read_flag = true;
     
                }
            }
            else if(dmx512_devp->num_break > 1)
            {
                    dmx512_devp->start_read_flag = false;
                    dmx512_devp->end_read_flag = true;
           
            }
        }
        
        uart_insert_char(&sw_uport->port, lsr, SUNXI_UART_LSR_OE, ch, flag);
ignore_char:
        lsr = serial_in(&sw_uport->port, SUNXI_UART_LSR);
    } while ((lsr & (SUNXI_UART_LSR_DR | SUNXI_UART_LSR_BI)) && (max_count-- > 0));

    SERIAL_DUMP(sw_uport, "Rx");
    spin_unlock(&sw_uport->port.lock);
    tty_flip_buffer_push(&sw_uport->port.state->port);
    spin_lock(&sw_uport->port.lock);

    return lsr;
}

 

3、寫應用程式進行驗證。

開啟設定串列埠uart1 波特率250000 8 N 2

 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include <termios.h>
#include <errno.h>
#include <signal.h>

#include <stdbool.h>


#define UART1_DEV_NAME  "/dev/ttyS1"  /*需根據實際埠修改*/
#define DMX512_DEV_NAME "/dev/dmx512_uart"
#define BUF_LEN 100
#define MAX_BUF 2048


int oflags =0;
int fd =-1;
char buff[MAX_BUF] ={0};



/**
*@brief  設定串列埠
*@param  fd:串列埠檔案描述符. 
         nSpeed:波特率,
         nBits:資料位 7 or 8, 
         nEvent:奇偶校驗位,
         nStop:停止位
*@return 失敗返回-1;成功返回0;
*/

int set_serial(int fd, int nSpeed, int nBits, char nEvent, int nStop)
{
    struct termios newttys1, oldttys1;

    /*儲存原有串列埠設定*/
    if(tcgetattr(fd, &oldttys1) != 0)
    {
        perror("Setupserial 1");
        return - 1;
    }
    memset(&newttys1, 0, sizeof(newttys1));
    //memcpy(&newttys1, &oldttys1, sizeof(newttys1));
    /*CREAD 開啟序列資料接收,CLOCAL並開啟本地連線模式*/
    newttys1.c_cflag |= (CLOCAL | CREAD);

    newttys1.c_cflag &=~CSIZE; /*設定資料位*/
    switch(nBits)    /*資料位選擇*/
    {
        case 7:
            newttys1.c_cflag |= CS7;
            break;
        case 8:
            newttys1.c_cflag |= CS8;
            break;
        default:break;
    }
    
    switch(nEvent)  /*奇偶校驗位*/
    {
        case '0':
            newttys1.c_cflag |= PARENB; /*開啟奇偶校驗*/
            newttys1.c_iflag |= (INPCK | ISTRIP); /*INPCK開啟輸入奇偶校驗,ISTRIP 去除字元的第八個位元*/
            newttys1.c_cflag |= PARODD; /*啟動奇校驗(預設為偶校驗)*/
            break;
        case 'E':
            newttys1.c_cflag |= PARENB; /*開啟奇偶校驗*/
            newttys1.c_iflag |= (INPCK | ISTRIP); /*INPCK開啟輸入奇偶校驗,ISTRIP 去除字元的第八個位元*/
            newttys1.c_cflag &= ~PARODD; /*啟動偶校驗*/
            break;
        case 'N':
            newttys1.c_cflag &= ~PARENB; /*無奇偶校驗*/
            break;
        default:break;
    }
    
    switch(nSpeed) /*設定波特率*/
    {
        case 2400:
            cfsetispeed(&newttys1, B2400);
            cfsetospeed(&newttys1, B2400);
            break;
        case 4800:
            cfsetispeed(&newttys1, B4800);
            cfsetospeed(&newttys1, B4800);
            break;
        case 9600:
            cfsetispeed(&newttys1, B9600);
            cfsetospeed(&newttys1, B9600);
            break;
        case 115200:
            cfsetispeed(&newttys1, B115200);
            cfsetospeed(&newttys1, B115200);
            break;
        case 250000:
            //ret = cfsetispeed(&newttys1, 0020001);
            //printf("reti = %d\n",ret);
            //ret = cfsetospeed(&newttys1, 0020001);        
            //printf("reto = %d\n",ret);
            newttys1.c_cflag |= 0020001;
            break;
        default :
            cfsetispeed(&newttys1, B9600);
            cfsetospeed(&newttys1, B9600);
            break;
    }
    
    /*設定停止位*/
    /*停止位為1,則清除CSTOPB,如停止位為2,則啟用CSTOPB*/
    if(nStop == 1)
    {
        newttys1.c_cflag &= ~CSTOPB;  /*預設為停止位1*/
    }
    else if(nStop == 2)
    {
        newttys1.c_cflag |= CSTOPB;
    }

    newttys1.c_iflag &=~(PARMRK); /*不設定的*/

    newttys1.c_iflag |= IGNBRK ; /*設定的*/
    printf("newttys1.c_iflag= 0x%\n",newttys1.c_iflag);


    /*設定最少字元和等待時間,對於接收字元和等待時間沒有特別的要求時*/
    newttys1.c_cc[VTIME] = 0; /*非規範模式讀取時的超時時間*/
    newttys1.c_cc[VMIN] = 0; /*非規範模式讀取時的最小字元數*/
    
    /*tcflush 清空終端未完成的輸入、輸出請求及資料
    TCIFLUSH表示清空正接收到的資料,且不讀取出來*/
    tcflush(fd, TCIFLUSH);

    /*啟用設定使其生效*/
    if((tcsetattr(fd, TCSANOW, &newttys1)) != 0)
    {
        perror("usart set error");
        return - 1;
    }

    return 0;
}

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

    int ret =-1;
    int i =0;
    int n =0;
    int len = BUF_LEN;
    int baud = 250000;
    int fd_dmx512 =-1;

    struct sigaction saio;
    
    if(argc !=2)
    {
        printf("arg is not 2,arg is app baud_rate\n");
    }
    if(argc == 2)
        baud = atoi(argv[1]);
    printf("baud =%d\n",baud);
    fd = open(UART1_DEV_NAME, O_RDWR | O_NOCTTY | O_NDELAY);
    if(fd < 0)
    {
        perror("Can't open uart1 port");
        return(void *)"uart1 dev error";
    }
    ret = set_serial(fd,baud, 8, 'N', 2); /*可能需要根據情況調整*/
    if(ret < 0)
    {
        printf("set_serial error\n");
        return -1;    
    }

    while(1)
    {
        fd_dmx512 =open(DMX512_DEV_NAME,O_RDONLY);
        if(fd_dmx512 < 0)
        {
            printf("open dmx512 device error\n");
            return -1;
        }
       memset(buff,0,sizeof(buff));
        printf("Read start\n");
        n = read(fd_dmx512,buff,600);
        printf("Read end\n");
        printf("num=%d :",n);
        for(i=0;i<n;i++)
            printf("%02x ",buff[i]);
        printf("\n");
    
        ret = close(fd_dmx512);
        if(ret < 0)
            printf("close error\n");
    
        sleep(5);
    }

    return 0;
}

 

通過測試後正常讀取到串列埠資料

 

 

 

 

DMX512協定解析

(1)採用RS-485匯流排收發器,差分電壓進行傳輸的,抗干擾能力強,訊號可以進行長距離傳輸;
(2)不論調光資料是否需要改變,主機都必須傳送控制訊號。
(3)由於資料框之間的時間小於1s,所以在1s內沒有收到新的資料框,說明訊號已經丟失;
(4)因為是資料是調光用的,使用環境是不做安全要求的裝置, 並且是不間斷傳輸的,所以不需要複雜的校驗。

dmx512協定串列埠波特率為250000

一個bit位 4us
8個位(Slot:x) 4*8=32us,x是從1到512

 

break 88us(範圍是88μs——1ms)
MAB(Mark After Break) 8us 兩個bit位的時間,高電平
start bit 4us 是低電平
Start Code(SC) 32us,8個位,是一段低電平,必須要有,串列埠表現中資料是0,接收時作頭的一部分
stop 8us 兩位結束,是高電平
MTBP 0-1s(MARK Time aftet slot,每一個資料間隔的空閒時間,是高電平,可以不要。

一幀資料包括 start + Slotx: + stop + MTBP = 4+32+8+0=44us

 

參考檔案

(19條訊息) DMX512協定解析_春風得意吃火鍋的部落格-CSDN部落格_dmx512協定標準

(19條訊息) tty驅動 read 過程梳理_0x460的部落格-CSDN部落格