Базовые вопросы
Q1. Загрузил SDK, все в порядке, компиляция проходит успешно. Однако не могу запустить программу, в чем проблема?
Если используете Keil + RT-Thread Env, то первое, что вам следует сделать после загрузки исходного кода, это пройти процесс конфигурирования через menuconfig.
Если используете RT-Studio IDE, то нужно открыть Settings перед созданием проекта; пройдите через все элементы конфигурации, отмените ненужное, оставив только ядро (kernel).
Сначала убедитесь, что система нормально запускается в минимальной конфигурации, используя для этого простейшую программу наподобие мигания светодиодом. После этого пошагово добавляйте нижележащие устройства или другие пакеты, проверяя работу на каждом шаге (menuconfig -> компиляция -> проверка).
Q2. Загрузил SDK, все в порядке, компиляция проходит успешно. Однако программа показывает Hard Fault on Thread, в чем проблема?
Сделайте все то же самое, что перечислено в ответе на Q1.
Q3. Загрузил SDK, но есть проблемы с компиляцией.
Сделайте все то же самое, что перечислено в ответе на Q1.
Kernel
Q1. Как правильно определить RT_NAME_MAX?
Чем меньше RT_NAME_MAX, тем меньше будет использоваться памяти. Например, если существует 100 объектов kernel, и имя объекта занимает 8, то всего на это будет потрачено 800 байт. В структуре rt_object находится определение объекта, где есть определение имени длиной RT_NAME_MAX байт и две переменные типа rt_uint8_t. RT_NAME_MAX может быть определено как 2n + 2.
Q2. RT_DEBUG
Не включайте отладку ядра (kernel debugging), если не понимаете, необходимо ли это в вашей ситуации.
Q3. Как правильно определить размер стека потока?
Этот вопрос имеет большое отношение к приложениям. и если это только система с минимальным ядром (minimal kernel system), потоком ожидания (idle thread), без разрешенных прерываний и работающих программ, то 256 для стека достаточно. Если добавляется код программы, вместе с механизмами блокировки, очередей, передачи сообщений, то лучше размер стека потока увеличить до 1024 или даже больше (подбирается опытным путем).
Q4. Как быстро вычислить значение, возвращаемое GET_PIN?
Мы знаем, что группирование GPIO чипов обычно начинается с PA, потом идут PB, PC, PD, PE, ... PZ. Также каждая такая группа портов содержит либо 16 бит, либо 8 бит (соответствует 16 IO и 8 IO). Упрощенная формула для GET_PIN для 16-битных портов:
(X - A) * 16 + n
Т. е. для A10 получится 10. Для C9 получается 2*16+9=41. H1 будет 7*16+1=113.
Для 8-битных портов формула будет:
(X - A) * 8 + n.
Q5. Не понимаю, чем отличаются hard timers, soft timers и hardware timers.
RT-Thread kernel определяет программные таймеры (software timer). Они, в отличие от аппаратных таймеров (hardware timer), не требуют задействования отдельного периферийного устройства timer, как и для реализации разных других функций типа сравнение интервалов (comparison) и захват сигнала (capture). Программный таймер просто задает интервал времени таймаута, и запускает callback-функцию при истечении этого интервала.
Программный таймер, определяемый RT-Thread, также делится на два типа "hard timer" и "soft timer". Первый тип выполняет свою callback-функцию в прерываниях SysTick. Этот тип используется для встроенных таймеров потока, и может использоваться также на уровне приложения, однако следует иметь в виду, что callback-функции вызываются из контекста прерываний.
Второй тип, который работает в потоке, может использоваться, когда слой приложения не требует высокой точности отслеживания интервала времени. Однако следует отметить, что поток, который определяет таймер, и поток, который выполняет callback-функцию таймера, являются двумя разными потоками.
Q6. Сколько памяти достаточно запросить для пула очереди сообщений (Message Queue)?
Очередь сообщений может создаваться разными способами.
rt_err_t rt_mq_init (rt_mq_t mq,
const char *name,
void *msgpool,
rt_size_t msg_size,
rt_size_t pool_size,
rt_uint8_t flag);
rt_mq_t rt_mq_create (const char *name,
rt_size_t msg_size,
rt_size_t max_msgs,
rt_uint8_t flag);
Если вы создаете очередь сообщений с помощью rt_mq_create, то пул очереди сообщений будет вычислен автоматически по размеру тела сообщения msg_size и максимальной длине очереди max_msgs.
Если для инициализации очереди сообщений используется rt_mq_init, то память пула msgpool для сообщений должна быть предоставлена пользователем. В этом случае следует обратить внимание на размер структуры pool_size. Необходимый объем пула памяти можно вычислить по формуле:
(RT_ALIGN(msg_size, RT_ALIGN_SIZE) + sizeof(struct rt_mq_message*)) * max_msgs
Здесь msg_size это размер тела сообщения, и max_msgs это максимальная емкость очереди сообщений.
Q7. Замечания по использованию очереди сообщений (message queue).
Хотя несколько API-функций наподобие rt_mq_send, rt_mq_urgent и rt_mq_recv имеют параметры размера size, обратите особое внимание, что передаете строго одинаковые значения аргумента, соответствующие msg_size, в rt_mq_init и rt_mq_create. Не следует менять размер параметра size.
Т. е. не используйте очередь сообщений, чтобы напрямую посылать данные слишком большого размера.
Q8. Объяснение смысла макроса INIT_xxx_EXPORT.
Отличительная особенность RT-Thread в том, что в функции main не делается никакая начальная конфигурация, для этого существует отдельный поток. Другие потоки автоматически запускаются через INIT_APP_EXPORT. Поначалу это слегка запутывает пользователя.
Всего RT-Thread определяет 6 процессов запуска (startup processes).
1. Подпрограммы инициализации платы, вызываемые в функции board_init(). Определение:
#define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn, "1")
2. Подпрограммы инициализации pre/device/component/env/app, вызываемые в init_thread (чисто программная инициализация). Определение:
#define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn, "2")
3. Инициализация устройства. Определение:
#define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn, "3")
4. Инициализация компонента (dfs, lwip, ...). Определение:
#define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn, "4")
5. Инициализация окружения (монтирование диска, ...). Определение:
#define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn, "5")
6. Инициализация приложения (приложение rtgui, и т. п. ...). Определение:
#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6")
INIT_BOARD_EXPORT запускается перед запуском планировщика (scheduler). Здесь происходит процесс начальной инициализации конфигурации периферии.
Остальные процессы запускаются основным потоком (в standard-версии RT-Thread, если используется поток main) после того, как запущен планировщик.
Эти процессы не полностью фиксированы, и кое-что можно подстроить. Например, можно перенести инициализацию LCD из DEVICE в BOARD, и сделать инициализацию emwin в PREV. Некоторые очереди сообщений также инициализируются в процессе ENV.
В большинстве случаев все инициализации можно провести в описанных выше процессах 1 .. 6. Однако существует вероятность возникновения конфликта.
Этой теме посвящен топик PR #5194 в RT-Thread Github Repository, где находятся некоторые полезные ссылки, и многие разработчики находят здесь советы по решению своих проблем.
Рационально сделать запуск устройств и компонентов на одном уровне, разрешая их зависимости с помощью мьютексов. В этом случае нужно подстроить порядок кода.
Q9. Как реализовать пробуждение потока с помощью rt_thread_suspend и rt_thread_resume?
Лучше всего избегать таких сценариев. В RT-Thread может быть 2 возможности, возникающие при переходе потока в состояние приостановки (suspend): в одном случае он сразу получит процессорное время, в другом случае он будет ждать освобождения ресурса, пока ресурс заблокирован. Не существует полного и прозрачного способа, чтобы потоки знали текущее взаимное состояние.
Предположим, что поток A хочет явно приостановить поток B, но A не знает, отказался ли в настоящий момент B от CPU, или он покидает ресурс для приостановки, или ресурc доступен и B пробуждается, выходя из состояния приостановки. Так или иначе, опасно переводить в приостановку другой поток, не зная его текущее состояние.
Единственный допустимый вариант может быть, когда поток B загружен больше всего, и сам не будет проявлять инициативу для блокировки, чтобы оказаться от процессорного времени. Кроме того, приоритет B относительно низкий, и тогда высокоприоритетный поток A может приостановить B при определенных условиях. Но тогда поток B должен повлиять на поток ожидания (idle thread).
Фактически необходимого функционала можно достичь более безопасными методами, используя традиционные механизмы синхронизации потоков - ожидание на семафоре или очереди. Поток B приостанавливает сам себя, ожидая поступления сигнала от A; поток A тогда разбудит поток B с помощью сигнала (публикуя семафор или ставя объект в очередь).
Q10. Команды list_thread (или ps) не могут проверить состояние потока?
Ошибки потока в столбце Error не могут иметь какой-либо ссылки. Здесь 0 это нормальное состояние, -2 показывает таймаут, и когда вы выполните rt_thread_mdelay, он превратится в -2. Однако это не означает ошибку.
Столбец status представляет текущее состояние потока. Однако из-за того, что команда list_thread (или ps) выполняется в контексте потока tshell, поток tshell по определению работает (состояние running). Поток ожидания (idle thread) не может быть приостановлен, он показывает состояние готовности (ready). Другие потоки могут быть в состоянии ready, но в большинстве случаев при нормальном сценарии они могут быть в состоянии приостановки (suspend), ожидая публикации семафора или очереди. Однако это не означает, что другие потоки приостановлены, и не управляются планировщиком.
Q11. Может ли таймер выполнять долгие операции?
Как упоминалось выше, существует 3 разновидности таймеров в RT-Thread, и у каждого свои характеристики.
Hardware timer: его callback-функция вызывается в прерывании, так что не рекомендуется, чтобы она выполняла какие-либо длительные действия.
Hard timer: также выполняет свои callback-функции в прерываниях, и не рекомендуется в них напрямую выполнять длительные операции.
Soft Timer: выполняется потоком таймера (timer thread), который вызовет callback-функции, и теоретически здесь могут выполняться долгие операции. Timer thread также является обычным потоком, у него есть собственный стек потока, свой приоритет, и т. п. Если некоторые операции независимы, то размещение их в определенном потоке совпадет с выполнением в потоке таймера.
Однако способ, которым текущий timer thread обрабатывает soft-таймеры, не подходит для выполнения длительных операций. Для этого требуются некоторые изменения.
Необходимо соответствующим образом подстроить приоритет timer thread относительно других потоков. Следует иметь в виду, что если создано несколько soft-таймеров, то длительное выполнение callback-функции одного из этих soft-таймеров может повлиять на работу других soft-таймеров, чего избежать не получится.