Драйвер двигателя застревает либо на высокой, либо на низкой скорости случайным образом. Выход двигателя перестает работать на основе входного сигнала акселерометра
Запуск двигателя постоянного тока (макс. вход 6,5 А), драйвера двигателя (пиковый выход MD10C 7 А) и Arduino Mega. По сути, я создаю стабилизированный объект на основе ввода акселерометра. Он отлично работает, но внезапно, очень случайным образом, он дает сбои и зависает в том режиме, в котором он был последним, поэтому, если это был двигатель A HIGH, а двигатель B LOW на скорости 200, он просто сделает это, и я должен отключить его. , введите пустой код, затем снова мой код, затем запустите его, все будет отлично работать, а затем повторите все сначала. Время, необходимое для того, чтобы он вышел из строя, является случайным, я не могу найти для него никаких закономерностей. вот мой код
//Код для акселерометра, управляющего закодированным DC
#include <Encoder.h>
#include <Wire.h>
#include <MPU6050.h>
#include <QueueArray.h>
//Класс для чтения энкодера
Encoder myEnc(19,18);
//I2C-адрес MPU-6050
const int MPU_addr=0x68;
const uint16_t t1_load = 0;
// Timer1 сравнить значение:
// Тактовая частота / Предварительный делитель / Требуемая частота = Сравните значение
// 16 МГц / 64 / 200 Гц = 1250
const uint16_t t1_comp = 1250;
// Буферизованные данные для отправки/получения в/из ISR
volatile bool isr_flag = false; //Используется для флага при срабатывании ISR
volatile unsigned long isr_time = 0; //Ввод обновляется ISR
volatile byte isr_motor_direction; //Вывод вычисляется и обновляется в цикле()
volatile byte isr_motor_speed = 0; // Вывод вычисляется и обновляется в цикле()
//инициализация MPU6050 (ускорение/гироскоп)
MPU6050 sensor;
//ускорение в направлении y
int16_t AcY;
//не используя
int16_t AcX, AcZ, GyX, GyY, GyZ, Tmp;
//новая переменная для отображения акселерометра в градусах
int yAng;
//Переменные для создания метода PID
int currentTheta;
float error;
int dt = 5;
long integral=0;
long deriv;
int previousError;
// ускорение на свету, всегда нужно 0
int setpoint = 0;
//изменить скорость двигателя
#define MOTOR_SPEED_PIN 3
//изменить направление двигателя
#define MOTOR_DIRECTION_PIN 2
#define MOTOR_DIRECTION_CW LOW
#define MOTOR_DIRECTION_CCW HIGH
void setup()
{
//Отключить глобальное прерывание
cli();
// Сброс регулятора Timer1 Control Reg A
TCCR1A = 0;
// Установить режим CTC
TCCR1B &= ~(1 << WGM13);
TCCR1B |= (1 << WGM12);
// Устанавливаем предскаляр 64
TCCR1B &= ~(1 << CS12);
TCCR1B |= (1 << CS11);
TCCR1B |= (1 << CS10);
// Сбросить Timer1 и установить значение для сравнения
TCNT1 = t1_load;
OCR1A = t1_comp;
// Разрешить прерывание сравнения Timer1
TIMSK1 = (1 << OCIE1A);
// Разрешить глобальное прерывание
sei();
Wire.begin();
Wire.beginTransmission(MPU_addr);
Wire.write(0x6B);
Wire.write(0);
Wire.endTransmission(true);
Serial.begin(9600);
pinMode(MOTOR_SPEED_PIN, OUTPUT);
pinMode(MOTOR_DIRECTION_PIN, OUTPUT);
sensor.initialize();
}
void loop()
{
// Начать критическую секцию
//
cli();
bool flag = isr_flag; // Копируем флаг ISR
isr_flag = false; // Сбросить флаг ISR
sei();
//
//Конец критической секции
if (flag == true)
{
DebugMsgTime("PID algorithm begin");
// Начать критическую секцию
//
//Получение копии данных, которые были изменены ISR
cli();
unsigned long time = isr_time;
sei();
//
//Конец критической секции
static unsigned long previous_time = 0;
previous_time = time;
// Чтение проводов
Wire.beginTransmission(MPU_addr);
Wire.write(0x3B); // начиная с регистра 0x3B (ACCEL_XOUT_H)
Wire.endTransmission(false);
Wire.requestFrom(MPU_addr,14,true);
//чтение каждой из 6 осей MPU6050 и темп.
AcX=Wire.read()<<8|Wire.read(); // 0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L)
//Только с использованием AcY
AcY=Wire.read()<<8|Wire.read(); // 0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L)
AcZ=Wire.read()<<8|Wire.read(); // 0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L)
Tmp=Wire.read()<<8|Wire.read(); // 0x41 (TEMP_OUT_H) & 0x42 (TEMP_OUT_L)
GyX=Wire.read()<<8|Wire.read(); // 0x43 (GYRO_XOUT_H) & 0x44 (GYRO_XOUT_L)
GyY=Wire.read()<<8|Wire.read(); // 0x45 (GYRO_YOUT_H) & 0x46 (GYRO_YOUT_L)
GyZ=Wire.read()<<8|Wire.read();
// преобразование AcY в yAng из единиц ускорения в градусы
yAng = map(AcY, -17000, 17000, -90, 90);
//Рассчитываем параметры двигателя локально
long pos = PID(yAng);
byte motor_direction = pos > 0 ? MOTOR_DIRECTION_CW : MOTOR_DIRECTION_CCW //убедитесь, что это правильно
pos = abs(pos);
pos = constrain(pos, 0, 100);
byte motor_speed = map(pos, 0, 100, 0, 200);
// Начать критическую секцию
//
// Обновить данные ISR, готовые к выводу при следующем прерывании.
// Это удерживает выходы заблокированными друг с другом и синхронизированными с прерыванием.
cli();
isr_motor_direction = motor_direction;
isr_motor_speed = motor_speed;
sei();
//
//Конец критической секции
DebugMsgTime("PID algorithm end");
}
}
ISR(TIMER1_COMPA_vect)
{
//Выход
digitalWrite(MOTOR_DIRECTION_PIN, isr_motor_direction);
analogWrite(MOTOR_SPEED_PIN, isr_motor_speed);
//Вход
isr_time = micros();
isr_flag = true;
}
//Цикл PID для поиска ошибки между заданным значением и currentTheta
long PID(int currentTheta)
{
error = setpoint - currentTheta;
integral = integral + error*dt;
deriv = (error-previousError)/dt;
long output = 1.9 * error; + .8 * integral + 40 * deriv; //2.8,.7,70 //1.3,.8,40
previousError = error;
return output;
}
void DebugMsgTime(const char *msg)
{
static unsigned long previous_time = 0;
unsigned long time = micros();
unsigned long delta_time = time - previous_time;
previous_time = time;
Serial.println(msg);
Serial.println(time);
Serial.println(delta_time);
}
1 ответ
Что произойдет, если условие while
не будет выполнено? т.е. если millis() - time
больше или равно 20. Это то, что вы хотели? Если в цикле while
слишком много других вещей, он может выйти из него и больше никогда не входить в него.
loop()
Я добавил вызов функции, выводящей отладочную информацию.
unsigned long time = 0;
void loop()
{
DebugMsgTime("loop() begin");
while (millis() - time < 20)
{
DebugMsgTime("while begin");
time = millis();
// delay(10); // Короткая задержка ОК.
delay(20); // Долгая задержка приводит к выходу из цикла while.
DebugMsgTime("while end");
}
DebugMsgTime("loop() end");
}
DebugMsgTime()
Эта функция выводит сообщение вместе с текущим временем и разницей во времени с момента последнего вызова.
void DebugMsgTime(const char *msg)
{
static unsigned long previous_time = 0;
unsigned long time = millis();
unsigned long delta_time = time - previous_time;
previous_time = time;
Serial.println(msg);
Serial.println(time);
Serial.println(delta_time);
}
Вывод отладки
Он отлично работает, но вдруг, очень случайно, он дает сбои и застревает в том режиме, в котором он был последним
Basic Encoder Test:
loop() begin
0
0
while begin // Работает отлично...
1
1
while end
2
1
while begin
12
10
while end
34
22 // но вдруг, очень случайно, глючит...
loop() end // и застревает в том режиме, в котором он был последним
54
20
loop() begin
74
20
loop() end
97
23
loop() begin
118
21
loop() end
Как видите, использование этого цикла while
— очень неточный способ генерировать регулярное время выборки.
loop() ИСПРАВЛЕНИЕ
Как указал @FerryBig в комментариях, вам нужно изменить сравнение времени <
в начале цикла. Вам также не нужен цикл while
, потому что loop()
уже вызывается циклически. Вместо этого поместите сравнение времени в оператор if
, что-то вроде этого, что даст вам приблизительные 20 мс:
void loop()
{
static unsigned long previous_time = 0;
unsigned long time = millis();
unsigned long delta_time = time - previous_time;
if (delta_time >= 20)
{
previous_time = time;
// Выполнить алгоритм PID.
}
}
Однако для ПИД-регулятора может потребоваться большая точность.
Прерывание таймера
Лучше использовать прерывание по таймеру для вызова процедуры обслуживания прерываний (ISR) через регулярные промежутки времени. Этот ISR будет делать две простые вещи в следующем порядке:
- Вывод:
digitalWrite(motor_direction);
иanalogWrite(3, mtr2);
- Ввод: установите флаг, указывающий, что провод гироскопа нуждается в чтении.
Затем, когда флаг установлен, основной loop()
будет считывать провод гироскопа и вычислять motor_direction
и mtr2
в состоянии готовности для следующего прерывания.
const uint16_t t1_load = 0;
// Timer1 сравнить значение:
// Тактовая частота / Предварительный делитель / Требуемая частота = Сравните значение
// 16 МГц / 64 / 10 Гц = 25000
const uint16_t t1_comp = 25000;
volatile bool isr_flag = false; // Используется для отметки, когда ISR обновил данные.
volatile unsigned long isr_time = 0; // Данные.
void setup()
{
// Отключить глобальное прерывание
cli();
// Сброс регулятора Timer1 Control Reg A
TCCR1A = 0;
// Установить режим CTC
TCCR1B &= ~(1 << WGM13);
TCCR1B |= (1 << WGM12);
// Устанавливаем предскаляр 64
TCCR1B &= ~(1 << CS12);
TCCR1B |= (1 << CS11);
TCCR1B |= (1 << CS10);
// Сбросить Timer1 и установить значение для сравнения
TCNT1 = t1_load;
OCR1A = t1_comp;
// Разрешить прерывание сравнения Timer1
TIMSK1 = (1 << OCIE1A);
// Разрешить глобальное прерывание
sei();
Serial.begin(9600);
}
void loop()
{
//
// НАЧАЛО КРИТИЧЕСКОЙ СЕКЦИИ (атомарной)
//
cli();
bool flag = isr_flag; // Копируем флаг ISR.
isr_flag = false; // Сбросить флаг ISR.
sei();
//
// КОНЕЦ КРИТИЧЕСКОЙ СЕКЦИИ
//
Serial.println(flag);
if (flag == true)
{
//
// НАЧАЛО КРИТИЧЕСКОЙ СЕКЦИИ (атомарной)
//
// Получить копию данных, которые были изменены ISR.
cli();
unsigned long time = isr_time;
sei();
//
// КОНЕЦ КРИТИЧЕСКОЙ СЕКЦИИ
//
static unsigned long previous_time = 0;
Serial.println(time - previous_time);
previous_time = time;
}
delay(25);
}
ISR(TIMER1_COMPA_vect)
{
//TCNT1 = t1_load; // Здесь не нужно перезагружать, потому что он перезагружается автоматически.
isr_time = micros();
isr_flag = true; // Указываем, что доступны новые данные.
}
ПИД-регулятор
Это полная реализация ПИД-регулятора. Это должно дать вам прочную основу для разработки алгоритма PID для вашего двигателя и фильтрации входных сигналов гироскопа.
//Код для акселерометра, управляющего закодированным DC
#include <Encoder.h>
#include <Wire.h>
#include <MPU6050.h>
#include <QueueArray.h>
//Класс для чтения энкодера
Encoder myEnc(19, 18);
// I2C-адрес MPU-6050 idk, если мне нужно
const int MPU_addr = 0x68;
const uint16_t t1_load = 0;
// Timer1 сравнить значение:
// Тактовая частота / Предварительный делитель / Требуемая частота = Сравните значение
// 16 МГц / 64 / 10 Гц = 25000
//const uint16_t t1_comp = 25000;
// Если алгоритм PID занимает менее 5 мс, можно запустить Timer1 с максимальной частотой около 200 Гц.
// 16 МГц / 64 / 200 Гц = 1250
const uint16_t t1_comp = 1250;
// Буферизованные данные для отправки/получения в/из ISR.
volatile bool isr_flag = false; // Используется для обозначения срабатывания ISR.
volatile unsigned long isr_time = 0; // Ввод обновляется ISR.
volatile byte isr_motor_direction = LOW; // Вывод вычисляется и обновляется в цикле().
volatile byte isr_motor_speed = 0; // Вывод вычисляется и обновляется в цикле().
//инициализация MPU6050 (ускорение/гироскоп)
MPU6050 sensor;
//ускорение в направлении y
int16_t AcY;
//не используя
int16_t AcX, AcZ, GyX, GyY, GyZ, Tmp;
//новая переменная для отображения акселерометра в градусах
int yAng;
//Переменные для создания метода PID
int currentTheta;
float error;
int dt = 5;
long integral = 0;
long deriv;
int previousError;
// ускорение на свету, всегда нужно 0
int setpoint = 0;
//это контакт включения (PWM) для запуска скорости двигателя
#define MOTOR_SPEED_PIN 3
//изменить направление двигателя
// ЗАДАЧА: проверить правильность значений LOW и HIGH.
#define MOTOR_DIRECTION_PIN 2
#define MOTOR_DIRECTION_CW LOW
#define MOTOR_DIRECTION_CCW HIGH
void setup()
{
// Отключить глобальное прерывание.
cli();
// Сброс регулятора Timer1 Control Reg A
TCCR1A = 0;
// Установить режим CTC
TCCR1B &= ~(1 << WGM13);
TCCR1B |= (1 << WGM12);
// Устанавливаем предскаляр 64
TCCR1B &= ~(1 << CS12);
TCCR1B |= (1 << CS11);
TCCR1B |= (1 << CS10);
// Сбросить Timer1 и установить значение для сравнения
TCNT1 = t1_load;
OCR1A = t1_comp;
// Разрешить прерывание сравнения Timer1
TIMSK1 = (1 << OCIE1A);
// Разрешить глобальное прерывание
sei();
Wire.begin();
Wire.beginTransmission(MPU_addr);
Wire.write(0x6B);
Wire.write(0);
Wire.endTransmission(true);
Serial.begin(9600);
Serial.println("Basic Encoder Test:");
//pinMode(MOTOR_SPEED_PIN, OUTPUT); // Не требуется для AnalogWrite().
pinMode(MOTOR_DIRECTION_PIN, OUTPUT);
sensor.initialize();
}
void loop()
{
//
// НАЧАЛО КРИТИЧЕСКОЙ СЕКЦИИ (атомарной)
//
cli();
bool flag = isr_flag; // Копируем флаг ISR.
isr_flag = false; // Сбросить флаг ISR.
sei();
//
// КОНЕЦ КРИТИЧЕСКОЙ СЕКЦИИ
//
//Serial.println(флаг);
if (flag == true)
{
DebugMsgTime("PID algorithm begin");
//
// НАЧАЛО КРИТИЧЕСКОЙ СЕКЦИИ (атомарной)
//
// Получить копию данных, которые были изменены ISR.
cli();
unsigned long time = isr_time;
sei();
//
// КОНЕЦ КРИТИЧЕСКОЙ СЕКЦИИ
//
static unsigned long previous_time = 0;
//Serial.println(время);
//Serial.println(время - предыдущее_время);
previous_time = time;
//
// Чтение провода гироскопа.
//
Wire.beginTransmission(MPU_addr);
Wire.write(0x3B); // начиная с регистра 0x3B (ACCEL_XOUT_H)
Wire.endTransmission(false);
Wire.requestFrom(MPU_addr, 14, true);
//чтение каждой из 6 осей MPU6050 и темп.
AcX = Wire.read() << 8 | Wire.read(); // 0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L)
//Только с использованием AcY
AcY = Wire.read() << 8 | Wire.read(); // 0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L)
AcZ = Wire.read() << 8 | Wire.read(); // 0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L)
Tmp = Wire.read() << 8 | Wire.read(); // 0x41 (TEMP_OUT_H) & 0x42 (TEMP_OUT_L)
GyX = Wire.read() << 8 | Wire.read(); // 0x43 (GYRO_XOUT_H) & 0x44 (GYRO_XOUT_L)
GyY = Wire.read() << 8 | Wire.read(); // 0x45 (GYRO_YOUT_H) & 0x46 (GYRO_YOUT_L)
GyZ = Wire.read() << 8 | Wire.read();
// преобразование AcY в yAng из единиц ускорения в градусы
yAng = map(AcY, -17000, 17000, -90, 90);
//
// Локальный расчет параметров двигателя.
//
long pos = PID(yAng);
byte motor_direction = pos > 0 ? MOTOR_DIRECTION_CW : MOTOR_DIRECTION_CCW; // TODO: Проверить правильность определений.
pos = abs(pos);
pos = constrain(pos, 0, 100);
byte motor_speed = map(pos, 0, 100, 0, 200);
//
// НАЧАЛО КРИТИЧЕСКОЙ СЕКЦИИ (атомарной)
//
// Обновить данные ISR, готовые к выводу при следующем прерывании.
// Это удерживает выходы заблокированными друг с другом и синхронизированными с прерыванием.
cli();
isr_motor_direction = motor_direction;
isr_motor_speed = motor_speed;
sei();
//
// КОНЕЦ КРИТИЧЕСКОЙ СЕКЦИИ
//
DebugMsgTime("PID algorithm end");
}
// задержка (1);
}
ISR(TIMER1_COMPA_vect)
{
// Выход.
digitalWrite(MOTOR_DIRECTION_PIN, isr_motor_direction);
analogWrite(MOTOR_SPEED_PIN, isr_motor_speed);
// Вход.
isr_time = micros();
isr_flag = true;
}
//Цикл PID для поиска ошибки между заданным значением и currentTheta
long PID(int currentTheta)
{
error = setpoint - currentTheta;
integral = integral + error * dt;
deriv = (error - previousError) / dt;
long output = 1.9 * error + .8 * integral + 40 * deriv; //2.8,.7,70 //1.3,.8,40
previousError = error;
return output;
}
void DebugMsgTime(const char *msg)
{
static unsigned long previous_time = 0;
unsigned long time = micros();
unsigned long delta_time = time - previous_time;
previous_time = time;
Serial.println(msg);
Serial.println(time);
Serial.println(delta_time);
}
И я думаю, что в вашем ПИД-регуляторе может быть опечатка:
long PID(int currentTheta)
{
...
long output = 1.9*error; + .8*integral + 40*deriv;
|
typo?
Это эквивалентно:
long output = 1.9*error;
Что не является ПИД-регулятором.
По какой-то причине это должно быть там. Если у меня нет этой точки с запятой, это не сработает. У меня был цикл while для запуска каждые 20 мс. Я думаю, что кто-то еще здесь сказал мне, что лучше использовать этот способ вместо задержки. Это сделало так, что время контролировало каждое время выполнения, а не каждый раз, когда оно получало информацию., @Thunder Dornhofer
@ThunderDornhofer Здесь точка с запятой заканчивает выражение. Таким образом, остальная часть строки не имеет никакого эффекта. Что именно вы подразумеваете под «если у меня нет этой точки с запятой, это не сработает»? Как именно это сейчас работает?, @chrisl
@ThunderDornhofer, я обновил свой пост, чтобы показать, почему ваш цикл while не работает. Лучшим методом генерации регулярных интервалов времени является использование прерываний таймера., @tim
@ThunderDornhofer Условия вашего цикла на самом деле кажутся обратными, если они основаны на цикле мигания Arduino без задержки. https://www.arduino.cc/en/Tutorial/BlinkWithoutDelay Ваш код работает, потому что вы фактически обновляете время новыми значениями. пример Тима не включает обновление времени, которое есть в вашем коде, @Ferrybig
@Ferrybig, извините, это была небрежная ошибка копирования и вставки, когда я обновлял свой пост. Я вернул код разрыва цикла., @tim
@tim цикл все еще прерывается так, как вы объяснили, но цикл фиксируется путем замены < на >, так как это предотвратит переход в цикл на 20 мс < и только затем войдет в цикл, сбросив «таймер», @Ferrybig
@Ferrybig, я согласен, я как раз собирался дополнить свой комментарий if (currentMillis - previousMillis >= interval)
из учебника, на который вы ссылались. Тем не менее, это все еще не так точно, как прерывание по таймеру., @tim
Спасибо за помощь. Это имело большой смысл. Я постараюсь исправить это сегодня., @Thunder Dornhofer
@tim Кажется, я прикрепил прерывание ISR? Я следовал учебнику парней. Я могу делать это неправильно. Я обновил свой исходный код, если вы могли бы взглянуть на него. Спасибо, @Thunder Dornhofer
@ThunderDornhofer, код таймера в setup()
хорош. Я добавил фрагмент кода в раздел «Прерывание по таймеру», который выводит время прерывания и использует «критические разделы» для извлечения данных, которые были изменены ISR. Но код в вашем ISR занимает слишком много времени для выполнения (более 3 мс), что будет мешать обновлению millis()
, потому что прерывания отключены во время ISR, а прерывания используются для обновления счетчика, который считывается 'millis ()'. Максимально допустимое значение составляет 2 мс. Алгоритм PID должен быть помещен в оператор if
в loop()
., @tim
@ThunderDornhofer, я добавил полную реализацию ПИД-регулятора. Есть пара комментариев TODO
, которые вам нужно проверить на правильность. Не уверен, на какой частоте вы хотите запустить таймер, но у меня максимум около 200 Гц. Надеюсь, это поможет., @tim
@tim Вау, Тим, это так помогло. Он отлично работает, когда я перемещаю свой датчик, но все еще кажется, что он дает сбой, из-за чего мне приходится нажимать кнопку сброса, когда датчик находится в состоянии покоя, движения нет. У вас есть идеи, что это может быть или как это исправить? Я обновил свой код., @Thunder Dornhofer
@tim Также побочный вопрос: если бы я хотел добавить в этот код больше, например кнопки, этот вывод также попал бы в метод ISR?, @Thunder Dornhofer
@tim Он работает намного лучше, но после моих тестов я обнаружил, что через некоторое время он снова перестанет работать. При просмотре последовательного монитора, когда он выполняет операторы печати, он читает «Начало алгоритма PID», затем время, затем дельта-время, а затем, когда он дает сбой, он останавливается и не печатает «Алгоритм PID». конец, @Thunder Dornhofer
- Как заставить двигатели постоянного тока работать одновременно?
- Adafruit Motor Shield v1 Нужна помощь. Запуск одновременно трех двигателей постоянного тока?
- Управление скоростью вентилятора с помощью библиотеки Arduino PID
- Использование аналогового входа для чтения кнопки
- Преобразование строки в массив символов
- Обратное вращение шагового двигателя
- Arduino uno + cnc Shield v3 + драйвер шагового двигателя A4988 + AccelStepper?
- Как управлять 4 двигателями постоянного тока с помощью Arduino?
Для чего нужен цикл while? Я не вижу в этом смысла, учитывая, что у вас нет другого кода внутри
void loop()
, но вне циклаwhile
, аvoid loop()
уже зацикливается. Также вы, кажется, читаете необработанные данные акселерометра, которые могут быть довольно шумными. Вы можете использовать дополнительный фильтр для объединения датчиков с гироскопом и получения более стабильных результатов. И, наконец, вы используете библиотеку для MPU, но тогда вы читаете данные напрямую через Wire, вместо того, чтобы использовать функции библиотек. Есть причина для этого?, @chrislКак бы я использовал дополнение? как бы это выглядело? Будет ли использование библиотеки лучше, чем чтение напрямую с провода? Я думал, что они одно целое., @Thunder Dornhofer
Вы можете найти дополнительный фильтр в Google. По сути, вы смешиваете ускорение с интегралом измерений гироскопа, чтобы получить более стабильные показания. О библиотеке: я думаю, что то же самое, просто было любопытно, почему вы сначала используете библиотеку, а потом нет., @chrisl