Я студент, который хочет грубо измерить задержку прерываний чипов серии ARM Cortex-M без использования осциллографа. Однако я столкнулся с очень необычной проблемой, которая меня крайне озадачила.
Во-первых, позвольте мне кратко обрисовать мой подход к измерениям. Я попытался примерно измерить задержку прерывания с помощью встроенного таймера микроконтроллера. Моя основная идея заключалась в следующем: перед входом в процедуру обслуживания прерываний (ISR) я сбрасывал таймер на ноль. Затем я генерировал запрос на прерывание и вводил ISR. Сразу при входе в ISR я читал значение таймера. В этот момент значение таймера может служить очень грубой оценкой задержки прерывания.
По определенным причинам мне нужно написать упомянутую функцию ISR на языке ассемблера. В основной функции я генерирую программное прерывание с помощью регистра SWIER, которое позволяет мне войти в написанный мною ISR.
Во время этого процесса я столкнулся со своеобразной проблемой. При написании ISR на языке ассемблера принято помещать в стек несколько регистров при входе в функцию, например от R4 до R11, и возвращать их обратно при выходе из функции. Следовательно, теоретически, если я не помещаю регистры в стек при входе в ISR (при условии, что я не использую эти регистры в ISR), грубое измерение задержки прерывания должно быть немного меньше по сравнению с тем, когда я действительно помещаю регистры в стек при входе в ISR. Однако в ходе своих экспериментов я обнаружил, что результаты были прямо противоположными. Если я помещаю регистры в стек при входе в ISR, измеренная задержка прерывания на самом деле будет немного меньше. Я проводил эксперименты с использованием разных IDE (соответствующих разным компиляторам), а также экспериментировал с разными продуктами, использующими один и тот же чип ARM (STM32103ZET6 и STM32F103C8T6), но результаты оставались неизменными. Это привело меня в полное недоумение.
В таблице ниже столбец CLOCK представляет частоту SYSCLK (HCLK). R4-R11 указывает значение счетчика таймера, полученное в результате чтения таймера, когда регистры R4–R11 были помещены в стек при входе в ISR. Аналогично, R4 представляет значение счетчика таймера, полученное, когда только регистр R4 был помещен в стек при входе в ISR. «Нестекированный» указывает значение счетчика таймера, полученное, когда ни один регистр не был помещен в стек при входе в ISR. Значения счетчика таймера в таблице получены из нескольких экспериментов.
Из приведенных выше экспериментальных результатов можно заметить, что во время процесса измерения, если в стек помещается только регистр R4, полученное значение счетчика таймера меньше по сравнению с тем, когда в стек помещаются регистры от R4 до R11 (что согласуется с теорией). и интуиция). Однако когда мы решаем не помещать регистры в стек, происходит нечто странное: полученное значение счетчика таймера оказывается больше, что противоречит нашей интуиции.
【MCU】: STM32F103C8T6
【IDE】: Keil uVision 5 V5.39.0.0
【MCU】: STM32F103ZET6
【IDE】: Keil uVision 5 V5.39.0.0
【MCU】: STM32F103ZET6
【IDE】: IAR для версии ARM 8.32.1.18631.
Из приведенных выше экспериментальных результатов можно заметить, что во время процесса измерения, если в стек помещается только регистр R4, полученное значение счетчика таймера меньше по сравнению с тем, когда в стек помещаются регистры от R4 до R11 (что согласуется с теорией). и интуиция). Однако когда мы решаем не помещать регистры в стек, происходит нечто странное: полученное значение счетчика таймера оказывается больше, что противоречит нашей интуиции.
Вот часть основного кода.
//main.c
/* USER CODE BEGIN WHILE */
while (1)
{
TIM2->CNT = 0; //set timer2 count=0
EXTI->SWIER |= 1; //call EXIT0 ISR
for(;i!=0;i--){} //delay
printf("[%5d]:TIM time t is :%d\n",line,t);
printf("***********[%5d]:*******\n",line);
line++;
i = COUNT_NUM;
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
;ISR.s
AREA ISR,CODE,READONLY
EXPORT EXTI0_IRQHandler
;EXTI0_IRQHandler is a global function name that is an ISR written in the Assembly language.
;To enable EXTI0_IRQHandler, the automatically generated EXTI0_IRQHandler function needs to be commented out in the stm32f1xx_it.c file.
IMPORT t
;t is a global variable used to store the value of the timer read in the ISR.
IMPORT exit_pr_addr
;exit_pr_addr is the global variable used to store the address of the EXTI_PR register.
;This address is used in the ISR in order to clear the interrupt request flag
IMPORT tim_cnt_addr
;tim_cnt_addr is a global variable used to store the address of the count register in the timer.
;This address is used in the ISR in order to read the count value of the timer
EXTI0_IRQHandler
;push {r4-r11}
;push {r4}
;;;t = TIM2->CNT
ldr r1,=tim_cnt_addr;read global var addr
ldr r2,[r1] ;read tim_cnt_addr value
ldr r2,[r2] ;read cnt value
ldr r0,=t ;get global var t addr
str r2,[r0] ;set t = TIM2->CNT
;;;clear interupt request flg
ldr r1,=exit_pr_addr ;read global var addr
ldr r1,[r1] ;read exit_pr_addr value
mov r3,#(1<<0) ;
str r3,[r1];
;pop {r4}
;pop {r4-r11}
bx lr
END
Некоторые настройки для компиляции с помощью Keil uVision 5 V5.39.0.0.
Я упаковал вышеупомянутый код проекта в STM32F103C8T6, который успешно компилируется под Keil uVision 5 V5.39.0.0. Под платформой IAR код проекта практически такой же, за исключением некоторых отличий в формате ISR, написанного на ассемблере, остальной код остаётся прежним. Эта часть кода приведена в файле ISR_FOR_IAR.s в моем вложении. Упомянутый выше код загружен на Google Диск как вложение .
Есть ли кто-нибудь, кто может объяснить эту странную проблему, которую я обнаружил?
В случае без push/pop вы, вероятно, запускаете ISR дважды из-за позднего сброса флага, вызвавшего прерывание. Поскольку два ISR выполняются один за другим, main печатает значение, сохраненное во втором прогоне (таким образом, примерно всю продолжительность ISR плюс «половину» второго, минус поправка на хвостовую цепочку).
Вы можете подтвердить или отклонить это, переместив всплывающее окно перед очисткой флага прерывания, или добавив счетчик, считающий число, или проверив флаг прерывания при входе в ISR, или очистив TIMx_CNT в ISR, или установив для TIMx_CNT значение какое-то заметное значение в ISR, или... вероятно, есть много способов.
P.S. Осциллограф или хотя бы дешевый логический анализатор (ЛА) – незаменимый инструмент для программирования микроконтроллеров; Я рекомендую вам получить немного. Один из способов проверить задержку/длительность прерываний — переключить вывод и измерить его — вы сами об этом упомянули.
@MasterLu: Вы неправильно поняли этот ответ. Он говорит о том, что часть ;;;clear interupt request flg
вашего кода происходит слишком поздно в прерывании, поэтому не вступает в силу отмена подтверждения прерывания до bx lr
. После возврата прерывания сигнал запроса на прерывание все еще активен, поэтому происходит еще одно прерывание. То, что делал основной поток, не имеет отношения к этому эффекту. Вы можете заставить свой ISR увеличивать указатель на то, где хранить результат таймера, чтобы временная метка второго прерывания не перезаписывала первую, и вы могли обнаружить, что это вообще происходит.
Другой способ обойти эту проблему — заставить ISR вращаться некоторое время после очистки флага запроса, чтобы чтение таймера происходило сразу, но мы откладываем выход из ISR до тех пор, пока очистка флага не вступит в силу. Но увеличение счетчика — хороший способ убедиться в наличии эффекта.
@PeterCordes Понятно, большое спасибо за объяснение. Это означает, что он генерировал вложенные прерывания внутри прерываний. Я доработаю свою программу по вашим предложениям, большое спасибо за ответ!
@MasterLu: Нет, не вложенно. Согласно этому ответу и странице, на которую он ссылается, второе прерывание происходит после bx lr
в первом, но до того, как оборудование вне ЦП отреагирует на сохранение в своем управляющем регистре. Если бы еще одно прерывание могло произойти до того, как вернулось это, они были бы вложены сколь угодно глубоко, никогда не достигая кода, который очищает флаг запроса.
@PeterCordes: Большое спасибо за вашу постоянную помощь и поддержку в ответах на мой вопрос. Ваша помощь была для меня невероятно ценна. Я переработаю свою программу. Еще раз спасибо.
@MasterLu, я писал о методах подтверждения или отклонения этого сценария, какой из них вы пробовали и каковы были результаты?
@wek: Извините, я еще не проверял, так как мне нужно купить новый адаптер USB-TTL. Как только у меня появятся условия для эксперимента, я сделаю это и сообщу вам о результатах. Спасибо за вашу помощь.
@wek: Как показали мои эксперименты, ваш ответ абсолютно правильный. Действительно большое спасибо!
Основываясь на моей экспериментальной проверке, причина этой проблемы точно такая же, как описано в ответе @wek.
Мой экспериментальный подход заключается в следующем: я использую глобальную переменную cnt
, которая инициализируется значением 0
. Каждый раз при вводе ISR я ставлю cnt = cnt + 1
. После выхода из ISR (в основной функции) я сделал сброс cnt = 0
.
//main.c
/* USER CODE BEGIN WHILE */
while (1)
{
TIM2->CNT = 0; //set timer2 count=0
EXTI->SWIER |= 1; //call EXIT0 ISR
for(;i!=0;i--){} //delay
printf("[%5d]:TIM time t is :%d\n",line,t);
printf("[%5d]: cnt is :%d\n",line,cnt);
printf("***********[%5d]:*******\n",line);
line++;
cnt=0; //cnt is global var
i = COUNT_NUM;
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
;ISR.s
AREA ISR,CODE,READONLY
EXPORT EXTI0_IRQHandler
IMPORT t
IMPORT exit_pr_addr
IMPORT tim_cnt_addr
IMPORT cnt
;cnt is a global variable used to store the number of times the ISR is entered.
EXTI0_IRQHandler
;push {r4-r11}
;push {r4}
;;;t = TIM2->CNT
ldr r1,=tim_cnt_addr;read global var addr
ldr r2,[r1] ;read tim_cnt_addr value
ldr r2,[r2] ;read cnt value
ldr r0,=t ;get global var t addr
str r2,[r0] ;set t = TIM2->CNT
;;;set cnt = cnt + 1
ldr r0, =cnt;
ldr r1,[r0]
add r1,r1,#1;
str r1,[r0]
;;;clear interupt request flag
ldr r1,=exit_pr_addr ;read global var addr
ldr r1,[r1] ;read exit_pr_addr value
mov r3,#(1<<0) ;
str r3,[r1];
;pop {r4}
;pop {r4-r11}
bx lr
END
Согласно моим экспериментам, когда я решаю поместить регистры в стек, напечатанное значение cnt
равно 1
. Однако, когда я решаю не помещать регистры в стек, напечатанное значение cnt
равно 2
. Это означает, что если регистры не помещаются в стек в ISR, ISR повторно вводится при выходе, в результате чего в ISR в общей сложности появляются 2 записи.
Большое спасибо за Ваш ответ. Однако я считаю маловероятным, что сценарий, о котором вы упомянули, с двойным входом в ISR, произойдет. Поскольку вся программа имеет только одно программное прерывание, я генерирую прерывание, манипулируя соответствующими управляющими битами регистров. После срабатывания программного прерывания я использую цикл for для имитации задержки. Я считаю, что пока эта задержка достаточно велика, при входе в ISR моя программа, скорее всего, все еще находится внутри цикла for, и после выхода из прерывания она также остается внутри цикла for.