基础概念
驱动分离:Linux内核中大量的驱动程序都采用总线(bus)、驱动(device_driver)和设备(device)模式,总线就是驱动和设备信息的月老,负责给两者牵线搭桥。
Linux内核使用结构体device_driver表示驱动,结构体device表示设备,bus_type结构体表示总线,代码如下:
// 总线
struct bus_type {
const char *name;
const char *dev_name;
struct device *dev_root;
struct device_attribute *dev_attrs; /* use dev_groups instead */
const struct attribute_group **bus_groups;
const struct attribute_group **dev_groups;
const struct attribute_group **drv_groups;
int (*match)(struct device *dev, struct device_driver *drv); // 重点关注
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*online)(struct device *dev);
int (*offline)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
const struct dev_pm_ops *pm;
const struct iommu_ops *iommu_ops;
struct subsys_private *p;
struct lock_class_key lock_key;
};
// 设备信息
struct device {
struct device *parent;
struct device_private *p;
struct kobject kobj;
const char *init_name; /* initial name of the device */
const struct device_type *type;
struct mutex mutex; /* mutex to synchronize calls to
* its driver.
*/
struct bus_type *bus; /* type of bus device is on */
struct device_driver *driver; /* which driver has allocated this
device */
void *platform_data; /* Platform specific data, device
core doesn't touch it */
void *driver_data; /* Driver data, set and get with
dev_set/get_drvdata */
struct dev_pm_info power;
struct dev_pm_domain *pm_domain;
#ifdef CONFIG_PINCTRL
struct dev_pin_info *pins;
#endif
#ifdef CONFIG_NUMA
int numa_node; /* NUMA node this device is close to */
#endif
u64 *dma_mask; /* dma mask (if dma'able device) */
u64 coherent_dma_mask;/* Like dma_mask, but for
alloc_coherent mappings as
not all hardware supports
64 bit addresses for consistent
allocations such descriptors. */
unsigned long dma_pfn_offset;
struct device_dma_parameters *dma_parms;
struct list_head dma_pools; /* dma pools (if dma'ble) */
struct dma_coherent_mem *dma_mem; /* internal for coherent mem
override */
#ifdef CONFIG_DMA_CMA
struct cma *cma_area; /* contiguous memory area for dma
allocations */
#endif
/* arch specific additions */
struct dev_archdata archdata;
struct device_node *of_node; /* associated device tree node */
struct fwnode_handle *fwnode; /* firmware device node */
dev_t devt; /* dev_t, creates the sysfs "dev" */
u32 id; /* device instance */
spinlock_t devres_lock;
struct list_head devres_head;
struct klist_node knode_class;
struct class *class;
const struct attribute_group **groups; /* optional groups */
void (*release)(struct device *dev);
struct iommu_group *iommu_group;
bool offline_disabled:1;
bool offline:1;
};
// 设备驱动
struct device_driver {
const char *name; // 如果通过名称匹配,重点关注
struct bus_type *bus;
struct module *owner;
const char *mod_name; /* used for built-in modules */
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
const struct of_device_id *of_match_table; // 如果使用设备树匹配,重点关注
const struct acpi_device_id *acpi_match_table; // ACPI匹配
int (*probe) (struct device *dev); // 重点关注,匹配成功后执行此函数
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups;
const struct dev_pm_ops *pm;
struct driver_private *p;
};
总线就是通过成员match函数完成驱动和设备的匹配,因此每个总线必须要实现此函数。
匹配过程
无论总线、驱动还是设备,都需要先注册,注册使用对应的xxxx_ register函数。驱动注册调用driver_register()函数,设备注册调用device_register()函数。
驱动匹配设备
驱动匹配设备时,存在两种情况:
- 设备已经存在
- 设备不存在
当设备不存在时,执行至driver_match_device函数处停止,此时驱动已经通过klist_add_tail等函数加入内核。
设备匹配驱动,与上述步骤类似。
platform设备驱动
为了解决在SOC中有些外设没有总线这个概念,但是又要使用总线、驱动和设备模型的问题, Linux提出了platform这个虚拟总线,相应的就有platform_driver和platform_device。
platform总线是bus_type类型的一个实例,其代码如下:
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match, // 关键函数,通过此函数完成驱动和设备的匹配
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
EXPORT_SYMBOL_GPL(platform_bus_type);
platform的驱动和设备通过platform_match函数完成匹配,其代码如下:
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/* When driver_override is set, only bind to the matching driver */
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);
/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
/* Then try to match against the id table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}
根据官方源码可知,有四种方法可以完成匹配:
- 通过设备树匹配
- ACPI方式
- 驱动中的id_table成员
- 驱动与设备的名称
下图是设备树匹配最终执行的函数。
因此如果使用设备树进行匹配,设备树中节点的compatible属性必须与驱动保持一致。
无论注册设备还是驱动,都需要调用相应xxx_register()函数,platform平台的设备和驱动也是如此。例如platform驱动注册以及其定义相关的代码如下:
struct platform_driver {
int (*probe)(struct platform_device *); // 重点关注
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};
int __platform_driver_register(struct platform_driver *drv,
struct module *owner)
{
drv->driver.owner = owner;
drv->driver.bus = &platform_bus_type;
if (drv->probe)
drv->driver.probe = platform_drv_probe;
if (drv->remove)
drv->driver.remove = platform_drv_remove;
if (drv->shutdown)
drv->driver.shutdown = platform_drv_shutdown;
return driver_register(&drv->driver);
}
EXPORT_SYMBOL_GPL(__platform_driver_register);
static int platform_drv_probe(struct device *_dev)
{
struct platform_driver *drv = to_platform_driver(_dev->driver);
struct platform_device *dev = to_platform_device(_dev);
int ret;
ret = of_clk_set_defaults(_dev->of_node, false);
if (ret < 0)
return ret;
ret = dev_pm_domain_attach(_dev, true);
if (ret != -EPROBE_DEFER) {
ret = drv->probe(dev); // 重点关注
if (ret)
dev_pm_domain_detach(_dev, true);
}
if (drv->prevent_deferred_probe && ret == -EPROBE_DEFER) {
dev_warn(_dev, "probe deferral not supported\n");
ret = -ENXIO;
}
return ret;
}
根据platform_driver定义可知,其基类是device_driver。
在注册驱动时,首先判断platform_driver中的成员probe是否定义,如果定义将device_driver基类中的成员probe赋值为platform_drv_probe,即匹配成功后执行的函数。在platform_drv_probe函数中将会调用platform_driver中的成员probe,即需要自行编写的内容。
总结:在编写驱动和设备程序时,需要重点关注platform_driver中的probe、device_driver中的of_match_table、device_driver和device中的name等成员。
程序框架
通过name进行匹配
程序分为两部分,一份驱动程序,一份设备程序。注意name必须保持一致。
驱动程序如下所示:
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/platform_device.h>
#include <linux/kern_levels.h>
#include <asm/uaccess.h>
#define PLATFORM_DEVICE_LED_NAME "leddevice"
#define PLATFORM_DEVICE_LED_COUNT 1
#define LEDOFF 0
#define LEDON 1
static void __iomem *ccm_ccgr1; // clock
static void __iomem *mux_gpio1_io3; // pin reuse
static void __iomem *pad_gpio1_io3; // attribute
static void __iomem *gpio1_gdir; // input/output
static void __iomem *gpio1_dr; // 0 / 1
struct platform_device_led_t {
dev_t devid; // device number
int major; // main device number
struct cdev cdev;
struct class *class;
struct device *dev;
};
struct platform_device_led_t platform_device_led;
/*
* @description : LED打开/关闭
* @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
* @return : 无
*/
void led0_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON){
val = readl(gpio1_dr);
val &= ~(1 << 3);
writel(val, gpio1_dr);
}else if(sta == LEDOFF){
val = readl(gpio1_dr);
val|= (1 << 3);
writel(val, gpio1_dr);
}
}
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int platform_device_led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &platform_device_led; /* 设置私有数据 */
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t platform_device_led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
return -EFAULT;
}
ledstat = databuf[0]; /* 获取状态值 */
if(ledstat == LEDON) {
led0_switch(LEDON); /* 打开LED灯 */
}else if(ledstat == LEDOFF) {
led0_switch(LEDOFF); /* 关闭LED灯 */
}
return 0;
}
const struct file_operations platform_device_led_fops = {
.owner = THIS_MODULE,
.open = platform_device_led_open,
.write = platform_device_led_write,
};
static int led_platform_driver_probe(struct platform_device *dev)
{
struct resource *mem[5];
int i;
u32 val = 0;
int ret;
// 1 get resources for a device
for(i = 0; i < dev->num_resources; ++ i) {
mem[i] = platform_get_resource(dev, IORESOURCE_MEM, i);
if(!mem[i]) {
return -EINVAL;
}
}
// 2 address map
ccm_ccgr1 = ioremap(mem[0]->start, resource_size(mem[0]));
if(!ccm_ccgr1) {
return -ENOMEM;
}
mux_gpio1_io3 = ioremap(mem[1]->start, resource_size(mem[1]));
if(!mux_gpio1_io3) {
return -ENOMEM;
}
pad_gpio1_io3 = ioremap(mem[2]->start, resource_size(mem[2]));
if(!pad_gpio1_io3) {
return -ENOMEM;
}
gpio1_gdir = ioremap(mem[3]->start, resource_size(mem[3]));
if(!gpio1_gdir) {
return -ENOMEM;
}
gpio1_dr = ioremap(mem[4]->start, resource_size(mem[4]));
if(!gpio1_dr) {
return -ENOMEM;
}
// 3 initialize device
// 3.1 open clock
val = readl(ccm_ccgr1);
val &= ~(3 << 26);
val |= (3 << 26);
writel(val, ccm_ccgr1);
// 3.2 gpio
writel(0x5, mux_gpio1_io3);
// 3.3 attribute
writel(0x10B0, pad_gpio1_io3);
// 3.4 output
val = readl(gpio1_gdir);
val &= ~(1 << 3);
val |= (1 << 3);
writel(val, gpio1_gdir);
// 3.5 output 1
val = readl(gpio1_dr);
val &= ~(1 << 3);
val |= (1 << 3);
writel(val, gpio1_dr);
// 4 register character device driver
// 4.1 create device number
if(!platform_device_led.major) {
ret = alloc_chrdev_region(&platform_device_led.devid, 0, PLATFORM_DEVICE_LED_COUNT, PLATFORM_DEVICE_LED_NAME);
if(ret == 0) {
platform_device_led.major = MAJOR(platform_device_led.devid);
}
} else {
platform_device_led.devid = MKDEV(platform_device_led.major, 0);
ret = register_chrdev_region(platform_device_led.devid, PLATFORM_DEVICE_LED_COUNT, PLATFORM_DEVICE_LED_NAME);
}
if(ret < 0) {
printk(KERN_ERR "Unable to register devid for platform_device_led\n");
goto out1;
}
// 4.2 init cdev
cdev_init(&platform_device_led.cdev, &platform_device_led_fops);
// 4.3 add cdev
ret = cdev_add(&platform_device_led.cdev, platform_device_led.devid, PLATFORM_DEVICE_LED_COUNT);
if(ret) {
printk(KERN_ERR "Unable to add cdev for platform_device_led\n");
goto out2;
}
// 4.4 create class
platform_device_led.class = class_create(THIS_MODULE, PLATFORM_DEVICE_LED_NAME);
if(IS_ERR(platform_device_led.class)) {
ret = PTR_ERR(platform_device_led.class);
goto out3;
}
// 4.5 create device
platform_device_led.dev = device_create(platform_device_led.class, NULL, platform_device_led.devid, NULL, PLATFORM_DEVICE_LED_NAME);
if(IS_ERR(platform_device_led.dev)) {
ret = PTR_ERR(platform_device_led.dev);
goto out4;
}
printk("led_platform_driver_probe\n");
return 0;
out4:
class_destroy(platform_device_led.class);
out3:
cdev_del(&platform_device_led.cdev);
out2:
unregister_chrdev_region(platform_device_led.devid, PLATFORM_DEVICE_LED_COUNT);
out1:
return ret;
}
static int led_platform_driver_remove(struct platform_device *dev)
{
iounmap(ccm_ccgr1);
iounmap(mux_gpio1_io3);
iounmap(pad_gpio1_io3);
iounmap(gpio1_dr);
iounmap(gpio1_gdir);
device_destroy(platform_device_led.class, platform_device_led.devid);
class_destroy(platform_device_led.class);
cdev_del(&platform_device_led.cdev);
unregister_chrdev_region(platform_device_led.devid, PLATFORM_DEVICE_LED_COUNT);
printk("led_platform_driver_remove\r\n");
return 0;
}
// led_platform_device definition and initialization
static struct platform_driver led_platform_driver = {
.probe = led_platform_driver_probe,
.remove = led_platform_driver_remove,
.driver = {
.name = "imx6ul_led",
},
};
// module load entry
static int __init leddriver_init(void)
{
printk("led_platform_driver_register\n");
return platform_driver_register(&led_platform_driver);
}
// module remove entry
static void __exit leddriver_exit(void)
{
platform_driver_unregister(&led_platform_driver);
}
module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("super_msl");
设备程序如下所示:
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/device.h>
#include <linux/platform_device.h>
// 寄存器物理地址
#define CCM_CCGR1_BASE (0x020C406C)
#define SW_MUX_GPIO1_IO3_BASE (0x020E0068)
#define SW_PAD_GPIO1_IO3_BASE (0x020E02F4)
#define GPIO1_GDIR_BASE (0x0209C004)
#define GPIO1_DR_BASE (0x0209C000)
#define REGISTER_LENGTH 4
// device resources definition
static struct resource led_platform_device_resources[] = {
[0] = {
.start = CCM_CCGR1_BASE,
.end = CCM_CCGR1_BASE + REGISTER_LENGTH -1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = SW_MUX_GPIO1_IO3_BASE,
.end = SW_MUX_GPIO1_IO3_BASE + REGISTER_LENGTH -1,
.flags = IORESOURCE_MEM,
},
[2] = {
.start = SW_PAD_GPIO1_IO3_BASE,
.end = SW_PAD_GPIO1_IO3_BASE + REGISTER_LENGTH -1,
.flags = IORESOURCE_MEM,
},
[3] = {
.start = GPIO1_GDIR_BASE,
.end = GPIO1_GDIR_BASE + REGISTER_LENGTH -1,
.flags = IORESOURCE_MEM,
},
[4] = {
.start = GPIO1_DR_BASE,
.end = GPIO1_DR_BASE + REGISTER_LENGTH -1,
.flags = IORESOURCE_MEM,
},
};
// when module remove, it will be used
static void led_platform_device_release(struct device *dev)
{
printk("led_platform_device_release\r\n");
}
// led_platform_device definition and initialization
static struct platform_device led_platform_device = {
.name = "imx6ul_led",
.id = -1,
.dev = {
.release = led_platform_device_release,
},
.num_resources = ARRAY_SIZE(led_platform_device_resources),
.resource = led_platform_device_resources,
};
// module load entry
static int __init leddevice_init(void)
{
return platform_device_register(&led_platform_device);
}
// module remove entry
static void __exit leddevice_exit(void)
{
platform_device_unregister(&led_platform_device);
}
module_init(leddevice_init);
module_exit(leddevice_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("super_msl");
通过设备树进行匹配
在设备树的根节点中加入相应节点,compatible属性必须与驱动程序保持一致。
驱动程序如下所示:
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/platform_device.h>
#include <linux/kern_levels.h>
#include <asm/uaccess.h>
#include <linux/of_gpio.h>
#define PLATFORM_DEVICE_LED_NAME "leddevicedts"
#define PLATFORM_DEVICE_LED_COUNT 1
#define LEDOFF 0
#define LEDON 1
struct platform_device_led_t {
dev_t devid; // device number
int major; // main device number
struct cdev cdev;
struct class *class;
struct device *dev;
struct device_node *np;
int led0;
};
struct platform_device_led_t platform_device_led;
/*
* @description : LED打开/关闭
* @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
* @return : 无
*/
void led0_switch(u8 sta)
{
if(sta == LEDON){
gpio_set_value(platform_device_led.led0, 0);
}else if(sta == LEDOFF){
gpio_set_value(platform_device_led.led0, 1);
}
}
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int platform_device_led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &platform_device_led; /* 设置私有数据 */
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t platform_device_led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
return -EFAULT;
}
ledstat = databuf[0]; /* 获取状态值 */
if(ledstat == LEDON) {
led0_switch(LEDON); /* 打开LED灯 */
}else if(ledstat == LEDOFF) {
led0_switch(LEDOFF); /* 关闭LED灯 */
}
return 0;
}
const struct file_operations platform_device_led_fops = {
.owner = THIS_MODULE,
.open = platform_device_led_open,
.write = platform_device_led_write,
};
static int led_platform_driver_probe(struct platform_device *dev)
{
int ret;
// 1 init io
// 1.1 get node
platform_device_led.np = of_find_node_by_path("/gpioled");
if(IS_ERR(platform_device_led.np)){
ret = PTR_ERR(platform_device_led.np);
goto out4;
}
// 1.2 get GPIO number
platform_device_led.led0 = of_get_named_gpio(platform_device_led.np, "led-gpio", 0);
if (!gpio_is_valid(platform_device_led.led0)) {
ret = -EINVAL;
goto out4;
}
gpio_request(platform_device_led.led0, "led0");
gpio_direction_output(platform_device_led.led0, 1);
// 2 register character device driver
// 2.1 create device number
if(!platform_device_led.major) {
ret = alloc_chrdev_region(&platform_device_led.devid, 0, PLATFORM_DEVICE_LED_COUNT, PLATFORM_DEVICE_LED_NAME);
if(ret == 0) {
platform_device_led.major = MAJOR(platform_device_led.devid);
}
} else {
platform_device_led.devid = MKDEV(platform_device_led.major, 0);
ret = register_chrdev_region(platform_device_led.devid, PLATFORM_DEVICE_LED_COUNT, PLATFORM_DEVICE_LED_NAME);
}
if(ret < 0) {
printk(KERN_ERR "Unable to register devid for platform_device_led\n");
goto out1;
}
// 2.2 init cdev
cdev_init(&platform_device_led.cdev, &platform_device_led_fops);
// 2.3 add cdev
ret = cdev_add(&platform_device_led.cdev, platform_device_led.devid, PLATFORM_DEVICE_LED_COUNT);
if(ret) {
printk(KERN_ERR "Unable to add cdev for platform_device_led\n");
goto out2;
}
// 2.4 create class
platform_device_led.class = class_create(THIS_MODULE, PLATFORM_DEVICE_LED_NAME);
if(IS_ERR(platform_device_led.class)) {
ret = PTR_ERR(platform_device_led.class);
goto out3;
}
// 2.5 create device
platform_device_led.dev = device_create(platform_device_led.class, NULL, platform_device_led.devid, NULL, PLATFORM_DEVICE_LED_NAME);
if(IS_ERR(platform_device_led.dev)) {
ret = PTR_ERR(platform_device_led.dev);
goto out4;
}
printk("led_platform_driver_probe\n");
return 0;
out4:
class_destroy(platform_device_led.class);
out3:
cdev_del(&platform_device_led.cdev);
out2:
unregister_chrdev_region(platform_device_led.devid, PLATFORM_DEVICE_LED_COUNT);
out1:
return ret;
}
static int led_platform_driver_remove(struct platform_device *dev)
{
gpio_set_value(platform_device_led.led0, 1);
gpio_free(platform_device_led.led0);
device_destroy(platform_device_led.class, platform_device_led.devid);
class_destroy(platform_device_led.class);
cdev_del(&platform_device_led.cdev);
unregister_chrdev_region(platform_device_led.devid, PLATFORM_DEVICE_LED_COUNT);
printk("led_platform_driver_remove\r\n");
return 0;
}
static const struct of_device_id leds_of_match[] = {
{.compatible = "atkalpha-gpioled"},
{/* Sentinel */}
};
// led_platform_device definition and initialization
static struct platform_driver led_platform_driver = {
.probe = led_platform_driver_probe,
.remove = led_platform_driver_remove,
.driver = {
.name = "imx6ul_led",
.of_match_table = leds_of_match,
},
};
// module load entry
static int __init leddriver_init(void)
{
printk("led_platform_driver_register\n");
return platform_driver_register(&led_platform_driver);
}
// module remove entry
static void __exit leddriver_exit(void)
{
platform_driver_unregister(&led_platform_driver);
}
module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("super_msl");